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 */
1016import {
1117 CustomEditor ,
@@ -16,6 +22,15 @@ import { matchesKey } from "@earendil-works/pi-tui";
1622
1723const 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+
1934export 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