Skip to content

Commit bb100b1

Browse files
claude-code-bestglm-5.2
andcommitted
fix: ESC 关闭 local-jsx 面板后添加 grace-period 防止误触 cancel
/workflows 等面板通过 ESC 关闭时,React unmount 与 chat:cancel keybinding 的 isActive 解除之间存在竞态窗口,导致同一按 ESC 会穿透到 onCancel 并中止正在执行的 Workflow 工具。 添加 500ms grace-period guard:面板关闭时打时间戳,onCancel 在窗口 内吞掉 ESC 并 reset,后续有意 ESC 仍正常取消。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
1 parent 0eabccc commit bb100b1

1 file changed

Lines changed: 33 additions & 0 deletions

File tree

src/screens/REPL.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)