@@ -1136,6 +1136,18 @@ export function REPL({
11361136 const abortControllerRef = useRef < AbortController | null > ( null ) ;
11371137 abortControllerRef . current = abortController ;
11381138
1139+ // Timestamp (ms) of the most recent local-jsx panel dismissal (e.g. ESC on
1140+ // /workflows). Used by onCancel's grace-period guard: the ESC that closes
1141+ // a local-jsx panel (or any quick follow-up ESC within the grace window)
1142+ // must not fall through to abortController.abort('user-cancel') — otherwise
1143+ // closing the /workflows panel via ESC would kill the in-flight Workflow
1144+ // tool. The chat:cancel keybinding's isActive gate (`!isLocalJSXCommand`)
1145+ // only shields the panel while it's mounted; once React commits the
1146+ // unmount, the next ESC reaches onCancel unguarded. This ref closes that
1147+ // race without touching keybinding registration order.
1148+ const LOCAL_JSX_CLOSE_CANCEL_GRACE_MS = 500 ;
1149+ const localJSXClosedAtRef = useRef ( 0 ) ;
1150+
11391151 // Track whether the last turn was user-aborted (Ctrl+C / Escape).
11401152 // When true, useGoalContinuation skips the continuation enqueue so
11411153 // interrupted turns don't spin into an unstoppable loop. Reset to
@@ -1355,6 +1367,9 @@ export function REPL({
13551367 if ( args ?. clearLocalJSX ) {
13561368 localJSXCommandRef . current = null ;
13571369 setToolJSXInternal ( null ) ;
1370+ // Stamp the dismissal so onCancel's grace-period guard can swallow
1371+ // the ESC that just dismissed the panel (and any quick follow-up).
1372+ localJSXClosedAtRef . current = Date . now ( ) ;
13581373 return ;
13591374 }
13601375 // Otherwise, keep the local JSX command visible - ignore tool updates
@@ -2534,6 +2549,24 @@ export function REPL({
25342549 return ;
25352550 }
25362551
2552+ // Grace-period guard: if a local-jsx panel (e.g. /workflows) was just
2553+ // dismissed via ESC, swallow the same / immediately-following ESC so it
2554+ // doesn't fall through to abortController.abort('user-cancel') and kill
2555+ // the in-flight Workflow tool. Single-press ESC closes the panel
2556+ // (handled by the panel's own useInput → onDone → setToolJSX); the
2557+ // chat:cancel keybinding's isActive gate shields while the panel is
2558+ // mounted but not in the React commit window right after unmount.
2559+ // Reset the stamp so a later, deliberate ESC still cancels normally.
2560+ if (
2561+ localJSXClosedAtRef . current !== 0 &&
2562+ Date . now ( ) - localJSXClosedAtRef . current < LOCAL_JSX_CLOSE_CANCEL_GRACE_MS
2563+ ) {
2564+ localJSXClosedAtRef . current = 0 ;
2565+ logForDebugging ( '[onCancel] suppressed: local-jsx panel just dismissed' ) ;
2566+ return ;
2567+ }
2568+ localJSXClosedAtRef . current = 0 ;
2569+
25372570 logForDebugging ( `[onCancel] focusedInputDialog=${ focusedInputDialog } streamMode=${ streamMode } ` ) ;
25382571
25392572 // Pause proactive mode so the user gets control back.
0 commit comments