mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-21 14:07:20 +08:00
Merge pull request #10510 from Kilo-Org/dust-cork
fix(cli): reset terminal modes on TUI exit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Use the fallback logo in old Windows terminal emulators while keeping the Unicode logo available over SSH.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Avoid leaving mouse and advanced keyboard modes enabled after exiting the TUI in mintty and MINGW terminals.
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]),
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user