From 190edc0a415789eec098fbdd8cab33983eef9b9e Mon Sep 17 00:00:00 2001 From: liruifengv Date: Fri, 17 Jul 2026 13:10:46 +0800 Subject: [PATCH] fix(tui): dismiss /btw panel before cancelling compaction on Esc and Ctrl+C --- .changeset/btw-panel-compaction-priority.md | 5 + .../src/tui/controllers/editor-keyboard.ts | 26 +++--- .../tui/controllers/editor-keyboard.test.ts | 92 ++++++++++++++++++- 3 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 .changeset/btw-panel-compaction-priority.md diff --git a/.changeset/btw-panel-compaction-priority.md b/.changeset/btw-panel-compaction-priority.md new file mode 100644 index 0000000000..fa1741b9b5 --- /dev/null +++ b/.changeset/btw-panel-compaction-priority.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix Esc and Ctrl+C cancelling compaction instead of closing an open /btw panel. diff --git a/apps/kimi-code/src/tui/controllers/editor-keyboard.ts b/apps/kimi-code/src/tui/controllers/editor-keyboard.ts index 5b6d95e195..76d0363f80 100644 --- a/apps/kimi-code/src/tui/controllers/editor-keyboard.ts +++ b/apps/kimi-code/src/tui/controllers/editor-keyboard.ts @@ -129,21 +129,23 @@ export class EditorKeyboardController { return; } - if (host.state.appState.isCompacting) { + // The btw panel stacks above the transcript, so Ctrl+C cancels/closes it + // before touching an in-flight compaction or stream. + if (host.btwPanelController.cancelRunning()) { this.clearPendingExit(); - - if (this.clearEditorTextIfPresent()) return; - - this.cancelCurrentCompaction(); return; } - - if (host.btwPanelController.cancelRunning()) { + if (host.btwPanelController.closeOrCancel()) { this.clearPendingExit(); return; } - if (host.btwPanelController.closeOrCancel()) { + + if (host.state.appState.isCompacting) { this.clearPendingExit(); + + if (this.clearEditorTextIfPresent()) return; + + this.cancelCurrentCompaction(); return; } @@ -184,12 +186,14 @@ export class EditorKeyboardController { this.clearPendingUndoEsc(); return; } - if (host.state.appState.isCompacting) { - this.cancelCurrentCompaction(); + // The btw panel stacks above the transcript, so Esc dismisses it before + // touching an in-flight compaction or stream. + if (host.btwPanelController.closeOrCancel()) { this.clearPendingUndoEsc(); return; } - if (host.btwPanelController.closeOrCancel()) { + if (host.state.appState.isCompacting) { + this.cancelCurrentCompaction(); this.clearPendingUndoEsc(); return; } diff --git a/apps/kimi-code/test/tui/controllers/editor-keyboard.test.ts b/apps/kimi-code/test/tui/controllers/editor-keyboard.test.ts index 090d47d503..8f15e68426 100644 --- a/apps/kimi-code/test/tui/controllers/editor-keyboard.test.ts +++ b/apps/kimi-code/test/tui/controllers/editor-keyboard.test.ts @@ -12,16 +12,24 @@ interface Harness { readonly editor: Record unknown) | undefined>; readonly openUndoSelector: ReturnType; readonly cancelRunningShellCommand: ReturnType; + readonly cancelCompaction: ReturnType; + readonly btwCancelRunning: ReturnType; + readonly btwCloseOrCancel: ReturnType; } function createHarness(options: { streamingPhase?: string; isCompacting?: boolean } = {}): Harness { const editor: Record unknown) | undefined> = { setHistoryFilter: vi.fn() as unknown as (...args: never[]) => unknown, setInputMode: vi.fn() as unknown as (...args: never[]) => unknown, + getText: vi.fn(() => '') as unknown as (...args: never[]) => unknown, + setText: vi.fn() as unknown as (...args: never[]) => unknown, }; const openUndoSelector = vi.fn(); const cancelRunningShellCommand = vi.fn(); - const session = { cancel: vi.fn(async () => {}) }; + const cancelCompaction = vi.fn(async () => {}); + const btwCancelRunning = vi.fn(() => false); + const btwCloseOrCancel = vi.fn(() => false); + const session = { cancel: vi.fn(async () => {}), cancelCompaction }; const host = { state: { @@ -35,7 +43,7 @@ function createHarness(options: { streamingPhase?: string; isCompacting?: boolea ui: { requestRender: vi.fn() }, }, session, - btwPanelController: { closeOrCancel: vi.fn(() => false) }, + btwPanelController: { cancelRunning: btwCancelRunning, closeOrCancel: btwCloseOrCancel }, openUndoSelector, cancelRunningShellCommand, } as unknown as EditorKeyboardHost; @@ -46,7 +54,15 @@ function createHarness(options: { streamingPhase?: string; isCompacting?: boolea ); controller.install(); - return { host, editor, openUndoSelector, cancelRunningShellCommand }; + return { + host, + editor, + openUndoSelector, + cancelRunningShellCommand, + cancelCompaction, + btwCancelRunning, + btwCloseOrCancel, + }; } function pressEscape(editor: Harness['editor']): void { @@ -55,6 +71,12 @@ function pressEscape(editor: Harness['editor']): void { (handler as () => void)(); } +function pressCtrlC(editor: Harness['editor']): void { + const handler = editor['onCtrlC']; + if (handler === undefined) throw new Error('onCtrlC handler not installed'); + (handler as () => void)(); +} + function pressNonEscape(editor: Harness['editor']): void { const handler = editor['onNonEscapeInput']; if (handler === undefined) throw new Error('onNonEscapeInput handler not installed'); @@ -123,6 +145,70 @@ describe('EditorKeyboardController double-Esc undo', () => { }); }); +describe('EditorKeyboardController btw panel priority', () => { + it('Esc closes the btw panel first while compacting, without cancelling compaction', () => { + const { editor, btwCloseOrCancel, cancelCompaction } = createHarness({ isCompacting: true }); + btwCloseOrCancel.mockReturnValue(true); + + pressEscape(editor); + + expect(btwCloseOrCancel).toHaveBeenCalledOnce(); + expect(cancelCompaction).not.toHaveBeenCalled(); + }); + + it('Esc cancels compaction on the next press once the btw panel is gone', () => { + const { editor, btwCloseOrCancel, cancelCompaction } = createHarness({ isCompacting: true }); + btwCloseOrCancel.mockReturnValueOnce(true); + + pressEscape(editor); + expect(cancelCompaction).not.toHaveBeenCalled(); + + pressEscape(editor); + expect(cancelCompaction).toHaveBeenCalledOnce(); + }); + + it('Esc cancels compaction directly when no btw panel is open', () => { + const { editor, btwCloseOrCancel, cancelCompaction } = createHarness({ isCompacting: true }); + + pressEscape(editor); + + expect(btwCloseOrCancel).toHaveBeenCalledOnce(); + expect(cancelCompaction).toHaveBeenCalledOnce(); + }); + + it('Ctrl+C cancels a running btw question first while compacting', () => { + const { editor, btwCancelRunning, cancelCompaction } = createHarness({ isCompacting: true }); + btwCancelRunning.mockReturnValue(true); + + pressCtrlC(editor); + + expect(btwCancelRunning).toHaveBeenCalledOnce(); + expect(cancelCompaction).not.toHaveBeenCalled(); + }); + + it('Ctrl+C closes an idle btw panel while compacting, without cancelling compaction', () => { + const { editor, btwCloseOrCancel, cancelCompaction } = createHarness({ isCompacting: true }); + btwCloseOrCancel.mockReturnValue(true); + + pressCtrlC(editor); + + expect(btwCloseOrCancel).toHaveBeenCalledOnce(); + expect(cancelCompaction).not.toHaveBeenCalled(); + }); + + it('Ctrl+C cancels compaction when no btw panel is open', () => { + const { editor, btwCancelRunning, btwCloseOrCancel, cancelCompaction } = createHarness({ + isCompacting: true, + }); + + pressCtrlC(editor); + + expect(btwCancelRunning).toHaveBeenCalledOnce(); + expect(btwCloseOrCancel).toHaveBeenCalledOnce(); + expect(cancelCompaction).toHaveBeenCalledOnce(); + }); +}); + describe('EditorKeyboardController shell history recall', () => { type Recall = (entry: string, direction: 1 | -1) => string | undefined; type Mock = ReturnType;