Skip to content

Commit 29d8aea

Browse files
committed
fix: do not override bash-mode or empty-editor Escape
Match Pi's built-in onEscape contract more closely: only arm double-Esc clear for non-empty non-bash drafts while idle, leave whitespace/empty, `!` bash mode, streaming, autocomplete, and abort paths to the app.
1 parent 10534b6 commit 29d8aea

1 file changed

Lines changed: 27 additions & 7 deletions

File tree

extensions/double-esc-clear.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22
* Double Escape clears a non-empty editor draft.
33
*
44
* Built-in double-Escape only acts on an *empty* editor (tree / fork / none via
5-
* `doubleEscapeAction`). When the editor has text, Escape is a no-op while idle.
6-
* This extension makes Escape → Escape (within 500ms) clear the draft instead.
5+
* `doubleEscapeAction`). When the editor has normal draft text, Escape is a
6+
* no-op while idle. This extension makes Escape → Escape (within 500ms) clear
7+
* that draft instead.
78
*
8-
* Streaming / bash abort paths are left to the app.
9+
* Must not steal these built-in Escape paths:
10+
* - streaming abort
11+
* - bash-running abort
12+
* - bash-mode (`!…`) single-Esc clear/exit
13+
* - autocomplete cancel
14+
* - empty / whitespace-only double-Esc (`doubleEscapeAction`)
915
*/
1016
import {
1117
CustomEditor,
@@ -16,6 +22,15 @@ import { matchesKey } from "@earendil-works/pi-tui";
1622

1723
const DOUBLE_ESC_MS = 500;
1824

25+
/** Draft Pi would leave alone on Escape (not empty, not `!` bash mode). */
26+
function isClearableDraft(text: string): boolean {
27+
const trimmed = text.trim();
28+
if (trimmed.length === 0) return false;
29+
// Bash mode is exited with a single Escape by the app.
30+
if (text.trimStart().startsWith("!")) return false;
31+
return true;
32+
}
33+
1934
export default function (pi: ExtensionAPI) {
2035
pi.on("session_start", (_event, ctx) => {
2136
if (ctx.mode !== "tui") return;
@@ -37,7 +52,6 @@ export default function (pi: ExtensionAPI) {
3752
let lastEscapeTime = 0;
3853

3954
editor.handleInput = (data: string) => {
40-
// Match configured interrupt binding if available, else raw Escape.
4155
const isEscape =
4256
typeof keybindings.matches === "function"
4357
? keybindings.matches(data, "app.interrupt")
@@ -46,16 +60,22 @@ export default function (pi: ExtensionAPI) {
4660
if (
4761
isEscape &&
4862
!editor.isShowingAutocomplete() &&
63+
// Streaming / non-idle abort stays with the app.
4964
ctx.isIdle() &&
50-
editor.getText().length > 0
65+
isClearableDraft(editor.getText())
5166
) {
5267
const now = Date.now();
5368
if (now - lastEscapeTime < DOUBLE_ESC_MS) {
69+
// Second Esc on a plain draft → clear. Built-in onEscape is a
70+
// no-op for this case, so we do not fall through.
5471
editor.setText("");
5572
lastEscapeTime = 0;
56-
} else {
57-
lastEscapeTime = now;
73+
return;
5874
}
75+
// First Esc: arm timer, then fall through so bash-running /
76+
// other app Escape handlers still run when applicable.
77+
lastEscapeTime = now;
78+
originalHandleInput(data);
5979
return;
6080
}
6181

0 commit comments

Comments
 (0)