diff --git a/.changeset/fix-ctrl-s-steer-input-history.md b/.changeset/fix-ctrl-s-steer-input-history.md new file mode 100644 index 0000000000..02febdfb5e --- /dev/null +++ b/.changeset/fix-ctrl-s-steer-input-history.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +修复 Ctrl-S steer 直接发送的消息不写入输入历史的问题,避免按 ↑ 无法召回。 diff --git a/apps/kimi-code/src/tui/controllers/editor-keyboard.ts b/apps/kimi-code/src/tui/controllers/editor-keyboard.ts index 73dabd66cc..3dfef4a366 100644 --- a/apps/kimi-code/src/tui/controllers/editor-keyboard.ts +++ b/apps/kimi-code/src/tui/controllers/editor-keyboard.ts @@ -40,6 +40,12 @@ export interface EditorKeyboardHost { harness?: KimiHarness | undefined; handleUserInput(text: string): void; + /** + * Append one submitted input to the persistent input history (↑ recall). + * `handleUserInput` writes history itself; paths that dispatch editor text + * directly (Ctrl-S steering the draft) must call this explicitly. + */ + persistInputHistory(text: string): void; readonly btwPanelController: BtwPanelController; readonly skillCommandMap: Map; steerMessage(session: Session, input: readonly SteerInputItem[]): void; @@ -401,7 +407,13 @@ export class EditorKeyboardController { host.state.queuedMessages = queued.filter( (m, index) => m.mode === 'bash' || (firstBundle !== -1 && index >= firstBundle), ); - if (!editorIsBash && !editorHasInlineSkills && firstBundle === -1) editor.setText(''); + if (!editorIsBash && !editorHasInlineSkills && firstBundle === -1) { + // A steered editor draft bypasses handleUserInput (and its input- + // history write) — persist it here, or ↑ recall loses Ctrl-S-sent + // input. Queued items were already persisted at submit time. + if (text.length > 0) host.persistInputHistory(text); + editor.setText(''); + } for (const run of runs) { if (run.kind === 'text') { host.steerMessage(session, run.items); diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 70a34bd8ea..697b12b885 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -1568,7 +1568,7 @@ export class KimiTUI { } } - private async persistInputHistory(text: string): Promise { + async persistInputHistory(text: string): Promise { const trimmed = text.trim(); if (trimmed.length === 0) return; if (trimmed === this.lastHistoryContent) 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 626728e781..03ba47f665 100644 --- a/apps/kimi-code/test/tui/controllers/editor-keyboard.test.ts +++ b/apps/kimi-code/test/tui/controllers/editor-keyboard.test.ts @@ -476,9 +476,11 @@ describe('EditorKeyboardController Ctrl-S steering', () => { queued: Array>; engineV2?: boolean; skillCommandMap?: Map; + model?: string; }) { const steerMessage = vi.fn(); const steerSkillActivation = vi.fn(); + const persistInputHistory = vi.fn(); const updateQueueDisplay = vi.fn(); const setText = vi.fn(); const editor: Record unknown) | undefined> = { @@ -493,7 +495,7 @@ describe('EditorKeyboardController Ctrl-S steering', () => { editor, activeDialog: null, queuedMessages: options.queued, - appState: { streamingPhase: 'waiting', isCompacting: false, model: 'k2' }, + appState: { streamingPhase: 'waiting', isCompacting: false, model: options.model ?? 'k2' }, footer: { setTransientHint: vi.fn() }, ui: { requestRender: vi.fn() }, }, @@ -502,8 +504,10 @@ describe('EditorKeyboardController Ctrl-S steering', () => { skillCommandMap: options.skillCommandMap ?? new Map(), steerMessage, steerSkillActivation, + persistInputHistory, updateQueueDisplay, validateMediaCapabilities: vi.fn(() => true), + releaseStagingMedia: vi.fn(), showError: vi.fn(), track: vi.fn(), btwPanelController: { @@ -513,7 +517,7 @@ describe('EditorKeyboardController Ctrl-S steering', () => { } as unknown as EditorKeyboardHost; const controller = new EditorKeyboardController( host, - undefined as unknown as ImageAttachmentStore, + { get: vi.fn(() => undefined), retainFileIds: vi.fn() } as unknown as ImageAttachmentStore, ); controller.install(); const onCtrlS = editor['onCtrlS']; @@ -524,11 +528,67 @@ describe('EditorKeyboardController Ctrl-S steering', () => { setText, steerMessage, steerSkillActivation, + persistInputHistory, updateQueueDisplay, onCtrlS: onCtrlS as () => void, }; } + it('persists a steered editor draft into input history', () => { + const { host, setText, steerMessage, persistInputHistory, onCtrlS } = createCtrlSHarness({ + editorText: 'fresh steer', + queued: [], + }); + + onCtrlS(); + + expect(steerMessage).toHaveBeenCalledWith(host.session, [ + { text: 'fresh steer', parts: undefined, imageAttachmentIds: undefined, stagingPaths: [] }, + ]); + expect(persistInputHistory).toHaveBeenCalledWith('fresh steer'); + expect(setText).toHaveBeenCalledWith(''); + }); + + it('does not re-persist queued items — they were persisted at submit time', () => { + const { steerMessage, persistInputHistory, onCtrlS } = createCtrlSHarness({ + editorText: '', + queued: [{ text: 'queued text', agentId: 'main' }], + }); + + onCtrlS(); + + expect(steerMessage).toHaveBeenCalled(); + expect(persistInputHistory).not.toHaveBeenCalled(); + }); + + it('does not persist the draft when steering is rejected and the draft stays', () => { + const { setText, steerMessage, persistInputHistory, onCtrlS } = createCtrlSHarness({ + editorText: 'fresh steer', + queued: [], + model: '', + }); + + onCtrlS(); + + expect(steerMessage).not.toHaveBeenCalled(); + expect(persistInputHistory).not.toHaveBeenCalled(); + expect(setText).not.toHaveBeenCalled(); + }); + + it('does not persist an inline-skill draft left in the editor for the grouped path', () => { + const { setText, persistInputHistory, onCtrlS } = createCtrlSHarness({ + editorText: 'check /skill:review', + queued: [], + engineV2: true, + skillCommandMap: new Map([['skill:review', 'review']]), + }); + + onCtrlS(); + + expect(persistInputHistory).not.toHaveBeenCalled(); + expect(setText).not.toHaveBeenCalled(); + }); + it('steers text as a message, skill items as activations, and keeps bash queued', () => { const { host, steerMessage, steerSkillActivation, updateQueueDisplay, onCtrlS } = createCtrlSHarness({