diff --git a/.changeset/drain-terminal-input-exit.md b/.changeset/drain-terminal-input-exit.md new file mode 100644 index 0000000000..36f40809b9 --- /dev/null +++ b/.changeset/drain-terminal-input-exit.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Prevent modified keyboard release sequences from appearing after exiting the CLI. diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index f265875387..1e239e50e8 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -558,6 +558,7 @@ export class KimiTUI { await this.harness.close(); this.sessionEventHandler.stopAllMcpServerStatusSpinners(); this.uninstallRainbowDance(); + await this.state.terminal.drainInput(); this.state.ui.stop(); if (this.onExit) { await this.onExit(exitCode); diff --git a/apps/kimi-code/test/tui/signal-handlers.test.ts b/apps/kimi-code/test/tui/signal-handlers.test.ts index f240f70fd1..d60d643c11 100644 --- a/apps/kimi-code/test/tui/signal-handlers.test.ts +++ b/apps/kimi-code/test/tui/signal-handlers.test.ts @@ -1,8 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { KimiTUI, type KimiTUIStartupInput } from '#/tui/kimi-tui'; +import { KimiTUI, type KimiTUIStartupInput, type TUIState } from '#/tui/kimi-tui'; interface SignalDriver { + state: TUIState; registerSignalHandlers(): void; unregisterSignalHandlers(): void; emergencyTerminalExit(): never; @@ -298,6 +299,27 @@ describe('KimiTUI signal handlers', () => { expect(process.listenerCount('SIGTERM')).toBe(beforeSigterm); }); + it('stop() drains terminal input before stopping the UI and exiting', async () => { + const { driver, tui } = makeDriver(); + const events: string[] = []; + const drainInput = vi.spyOn(driver.state.terminal, 'drainInput').mockImplementation(async () => { + events.push('drain'); + }); + const uiStop = vi.spyOn(driver.state.ui, 'stop').mockImplementation(() => { + events.push('ui.stop'); + }); + tui.onExit = vi.fn(async () => { + events.push('exit'); + }); + + await tui.stop(); + + expect(drainInput).toHaveBeenCalledOnce(); + expect(uiStop).toHaveBeenCalledOnce(); + expect(tui.onExit).toHaveBeenCalledOnce(); + expect(events).toEqual(['drain', 'ui.stop', 'exit']); + }); + it('start() unregisters signal handlers when initialization throws', async () => { const { tui } = makeDriver(); // Force the very first awaited call inside start() to reject. We don't