diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e10eaeea6..6a3099a10 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -192,6 +192,9 @@ - Fixed status-line redraws crashing when tool-call arguments contain `BigInt` values. - Fixed committed native scrollback rows retaining the previous palette after committed theme switches; direct-terminal theme swaps now replay the transcript so scrolling history matches the live viewport without making theme previews destructive. +### Fixed + +- Fixed Esc preserving typed TUI prompt drafts instead of clearing unrecoverable input. ([#3869](https://github.com/can1357/oh-my-pi/issues/3869)) ## [16.2.7] - 2026-06-30 diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index 5d1e7e451..9dc99d3da 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -387,9 +387,7 @@ export class InputController { } else if (this.ctx.session.isStreaming) { this.#handleStreamingEscape(); } else if (this.ctx.editor.getText().trim()) { - // Esc with typed text clears the draft instead of (or before) any double-Esc action - this.ctx.editor.setText(""); - this.ctx.ui.requestRender(); + // Esc must not destroy an in-progress draft; it only disarms a previous empty-editor Esc. this.ctx.lastEscapeTime = 0; this.#clearStreamingEscapeArm(); } else { diff --git a/packages/coding-agent/test/input-controller-escape.test.ts b/packages/coding-agent/test/input-controller-escape.test.ts index 3da064a29..c00fa1442 100644 --- a/packages/coding-agent/test/input-controller-escape.test.ts +++ b/packages/coding-agent/test/input-controller-escape.test.ts @@ -672,7 +672,7 @@ describe("InputController escape behavior", () => { expect(ctx.showTreeSelector).not.toHaveBeenCalled(); expect(spies.resetDisplay).toHaveBeenCalledTimes(1); }); - it("clears typed editor text on Esc without opening selectors or aborting", () => { + it("preserves typed editor text on Esc without opening selectors or aborting", () => { const { ctx, editor, spies } = createContext(); const controller = new InputController(ctx); @@ -680,24 +680,26 @@ describe("InputController escape behavior", () => { editor.setText("draft message"); editor.onEscape?.(); - expect(editor.getText()).toBe(""); - expect(spies.requestRender).toHaveBeenCalledTimes(1); + expect(editor.getText()).toBe("draft message"); + expect(spies.requestRender).not.toHaveBeenCalled(); expect(ctx.showTreeSelector).not.toHaveBeenCalled(); expect(ctx.showUserMessageSelector).not.toHaveBeenCalled(); expect(spies.resetDisplay).not.toHaveBeenCalled(); expect(spies.abort).not.toHaveBeenCalled(); }); - it("does not treat the Esc after a text-clearing Esc as a double-Esc", () => { + it("does not treat the Esc after a text-preserving Esc as a double-Esc", () => { const { ctx, editor } = createContext(); const controller = new InputController(ctx); controller.setupKeyHandlers(); editor.onEscape?.(); // empty editor: arms double-Esc timer editor.setText("draft"); - editor.onEscape?.(); // clears text, must also reset the timer + editor.onEscape?.(); // preserves text, must also reset the timer + editor.setText(""); editor.onEscape?.(); // empty again: should only re-arm, not trigger + expect(ctx.showTreeSelector).not.toHaveBeenCalled(); expect(ctx.showUserMessageSelector).not.toHaveBeenCalled(); }); });