Merge pull request #10510 from Kilo-Org/dust-cork

fix(cli): reset terminal modes on TUI exit
This commit is contained in:
Catriel Müller
2026-05-22 11:46:09 -03:00
committed by GitHub
7 changed files with 175 additions and 22 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Use the fallback logo in old Windows terminal emulators while keeping the Unicode logo available over SSH.
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Avoid leaving mouse and advanced keyboard modes enabled after exiting the TUI in mintty and MINGW terminals.
+3 -2
View File
@@ -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,
@@ -1,21 +1,55 @@
// kilocode_change - new file
import fs from "node:fs"
/**
* 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[?9l", // disable X10 mouse tracking
"\x1b[?1000l", // disable normal mouse tracking
"\x1b[?1001l", // disable highlight mouse tracking
"\x1b[?1002l", // disable button-event mouse tracking
"\x1b[?1003l", // disable any-event mouse tracking (all movement)
"\x1b[?1005l", // disable UTF-8 extended mouse mode
"\x1b[?1006l", // disable SGR extended mouse mode
"\x1b[?1007l", // disable alternate scroll mode
"\x1b[?1015l", // disable RXVT mouse mode
"\x1b[<u", // pop/disable Kitty keyboard protocol
"\x1b[?1016l", // disable SGR pixel mouse mode
"\x1b[?2004l", // disable bracketed paste
"\x1b[?1004l", // disable focus tracking
"\x1b[?1l", // disable application cursor keys
"\x1b>", // disable application keypad mode
"\x1b[?66l", // disable numeric keypad application mode
"\x1b[>4;0m", // reset xterm modifyOtherKeys
...(kitty() ? ["\x1b[<u"] : []), // pop/disable Kitty keyboard protocol
"\x1b[?25h", // show cursor
"\x1b[0m", // reset text attributes
]
}
export function resetTerminalState() {
try {
process.stdout.write(sequences.join(""))
fs.writeSync(process.stdout.fd, sequences().join(""))
} catch (err) {
console.error("resetTerminalState failed", err)
}
+10 -4
View File
@@ -39,14 +39,20 @@ function flag(value: string | undefined) {
if (no.has(key)) return false
}
function windows(env: NodeJS.ProcessEnv) {
if (env.WT_SESSION) return true
if (env.TERM_PROGRAM === "vscode") return true
if (env.WEZTERM_PANE) return true
if (env.TERM_PROGRAM === "WezTerm") return true
return false
}
export function supports(env = process.env, platform = process.platform) {
const override = flag(env.KILO_UNICODE_LOGO)
if (override !== undefined) return override
// Terminals do not expose font glyph coverage over SSH, so prefer the safe logo for remote sessions.
if (env.TERM === "dumb") return false
if (env.SSH_TTY) return false
if (env.SSH_CLIENT) return false
if (env.SSH_CONNECTION) return false
// Old Windows Console Host cannot render the sextant glyphs used by the modern logo.
if (platform === "win32") return windows(env)
if (env.ConEmuPID) return false
if (env.ANSICON) return false
return true
+18 -10
View File
@@ -2,15 +2,23 @@ import { describe, expect, test } from "bun:test"
import { plain, session, supports, tui } from "../../src/kilocode/cli/logo"
describe("kilocode logo", () => {
test("falls back on remote terminals", () => {
expect(supports({ SSH_TTY: "/dev/pts/0" }, "linux")).toBe(false)
expect(supports({ SSH_CLIENT: "127.0.0.1 12345 22" }, "linux")).toBe(false)
expect(supports({ SSH_CONNECTION: "127.0.0.1 12345 127.0.0.1 22" }, "linux")).toBe(false)
test("allows remote terminals", () => {
expect(supports({ SSH_TTY: "/dev/pts/0" }, "linux")).toBe(true)
expect(supports({ SSH_CLIENT: "127.0.0.1 12345 22" }, "linux")).toBe(true)
expect(supports({ SSH_CONNECTION: "127.0.0.1 12345 127.0.0.1 22" }, "linux")).toBe(true)
})
test("allows Windows Terminal locally", () => {
expect(supports({}, "win32")).toBe(true)
expect(supports({ WT_SESSION: "session" }, "linux")).toBe(true)
test("falls back on old Windows terminals", () => {
expect(supports({}, "win32")).toBe(false)
expect(supports({ ANSICON: "1" }, "win32")).toBe(false)
expect(supports({ ConEmuPID: "123" }, "win32")).toBe(false)
})
test("allows modern Windows terminals", () => {
expect(supports({ WT_SESSION: "session" }, "win32")).toBe(true)
expect(supports({ TERM_PROGRAM: "vscode" }, "win32")).toBe(true)
expect(supports({ WEZTERM_PANE: "1" }, "win32")).toBe(true)
expect(supports({ TERM_PROGRAM: "WezTerm" }, "win32")).toBe(true)
})
test("allows an override", () => {
@@ -20,12 +28,12 @@ describe("kilocode logo", () => {
test("uses modern and fallback logo variants", () => {
expect(tui({ KILO_UNICODE_LOGO: "1" }, "linux").join("\n")).toContain("🬺🬏")
expect(tui({ SSH_TTY: "/dev/pts/0" }, "linux").join("\n")).not.toContain("🬺🬏")
expect(plain({ SSH_TTY: "/dev/pts/0" }, "linux").join("\n")).not.toContain("🬁🬬")
expect(tui({}, "win32").join("\n")).not.toContain("🬺🬏")
expect(plain({}, "win32").join("\n")).not.toContain("🬁🬬")
})
test("formats child session exit logo", () => {
const out = session("Title", "ses_test", "<dim>", "<reset>", { SSH_TTY: "/dev/pts/0" }, "linux")
const out = session("Title", "ses_test", "<dim>", "<reset>", {}, "win32")
expect(out).toContain("<dim>Title<reset>")
expect(out).not.toContain("🬺🬏")
})
@@ -0,0 +1,94 @@
import { afterEach, expect, test } from "bun:test"
import { kitty, sequences } from "../../src/kilocode/cli/cmd/tui/util/terminal"
const keys = ["TERM_PROGRAM", "MSYSTEM", "KILO_DISABLE_KITTY_KEYBOARD", "KILO_ENABLE_KITTY_KEYBOARD"] as const
type Key = (typeof keys)[number]
const saved = Object.fromEntries(keys.map((key) => [key, process.env[key]])) as Record<Key, string | undefined>
function env(input: Partial<Record<Key, string | undefined>>) {
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[<u")
})
test("disables Kitty keyboard in mintty", () => {
env({ TERM_PROGRAM: "mintty" })
expect(kitty()).toBe(false)
expect(sequences()).not.toContain("\x1b[<u")
})
test("disables Kitty keyboard in MSYS shells", () => {
env({ MSYSTEM: "MINGW64" })
expect(kitty()).toBe(false)
expect(sequences()).not.toContain("\x1b[<u")
})
test("allows explicitly enabling Kitty keyboard", () => {
env({ KILO_ENABLE_KITTY_KEYBOARD: "1", MSYSTEM: "MINGW64" })
expect(kitty()).toBe(true)
expect(sequences()).toContain("\x1b[<u")
})
test("allows explicitly disabling Kitty keyboard", () => {
env({ KILO_DISABLE_KITTY_KEYBOARD: "1", KILO_ENABLE_KITTY_KEYBOARD: "1" })
expect(kitty()).toBe(false)
expect(sequences()).not.toContain("\x1b[<u")
})
test("resets common terminal input modes", () => {
env({ KILO_DISABLE_KITTY_KEYBOARD: "1" })
expect(sequences()).toEqual(
expect.arrayContaining([
"\x1b[?9l",
"\x1b[?1000l",
"\x1b[?1001l",
"\x1b[?1002l",
"\x1b[?1003l",
"\x1b[?1005l",
"\x1b[?1006l",
"\x1b[?1007l",
"\x1b[?1015l",
"\x1b[?1016l",
"\x1b[?2004l",
"\x1b[?1004l",
"\x1b[?1l",
"\x1b>",
"\x1b[?66l",
"\x1b[>4;0m",
"\x1b[?25h",
"\x1b[0m",
]),
)
})