[BUG] [Claude Code for VS Code] ESC during IME composition cancels the running task in VS Code extension (root cause located)

Open 💬 1 comment Opened Jun 9, 2026 by GrandZhuo

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Summary

When using a CJK IME (Chinese / Japanese / Korean) in the Claude Code for VS Code extension, pressing ESC to dismiss the IME candidate window also unintentionally cancels the currently running Claude task.

This is a re-report of #41772 (closed as not planned), with runtime evidence + source-level root cause + a 1-line minimal patch, hoping the issue can be reconsidered now that the fix cost is shown to be trivial.

Environment

  • Claude Code for VS Code extension: v2.1.169 (Anthropic.claude-code-2.1.169-darwin-arm64)
  • VS Code: latest stable
  • OS: macOS (Apple Silicon)
  • IME: any system / third-party Chinese IME (Sogou, Microsoft Pinyin, macOS built-in pinyin all reproduce)

Reproduction

  1. Start any Claude task that takes a few seconds (e.g., ask it to read a large file).
  2. While the task is running, type a few letters with a Chinese IME so that the candidate window appears.
  3. Notice you typed the wrong pinyin and press ESC to dismiss the candidate window.
  4. Bug: the running Claude task is interrupted at the same time.

Expected behavior

ESC pressed during IME composition should only be consumed by the IME (close the candidate window / cancel composition) and should not interrupt the running task — consistent with how the Enter key in the same input field already correctly checks isComposing.

Runtime evidence

Captured via Developer: Toggle Keyboard Shortcuts Troubleshooting while reproducing the bug. I typed df to bring up the IME candidate window, then pressed ESC:

17:52:17.678 keydown 'd', keyCode: 229
17:52:17.679 Converted to KeyInComposition (114)
17:52:17.679 Resolving [KeyD]
17:52:17.679 From 1 keybinding entries, no when clauses matched

17:52:17.732 keydown 'f', keyCode: 229
17:52:17.732 Converted to KeyInComposition (114)
17:52:17.732 Resolving [KeyF]
17:52:17.732 From 1 keybinding entries, no when clauses matched

17:52:19.493 keydown Escape, keyCode: 229       ← the relevant one
17:52:19.493 Converted to KeyInComposition (114) ← VS Code knows IME is composing
17:52:19.493 Resolving [Escape]
17:52:19.494 From 89 keybinding entries, no when clauses matched

Three things this log proves:

  1. VS Code's KeybindingService correctly identified the ESC as KeyInComposition (keyCode 229 → internal 114).
  2. None of the 89 ESC keybindings matched (and as confirmed below, this extension registers no plain escape keybinding at all).
  3. The task was still cancelled. Therefore the cancellation must come from a JS listener inside the webview, bypassing VS Code's keybinding system.

Confirming no VS Code keybinding is responsible

package.json of the extension declares only the following ESC-related keybindings (none is plain escape):

{ "command": "claude-vscode.focus",  "key": "cmd+escape", ... }
{ "command": "claude-vscode.blur",   "key": "cmd+escape", ... }
{ "command": "claude-vscode.editor.open", "key": "cmd+shift+escape", ... }
{ "command": "claude-vscode.terminal.open.keyboard", "key": "cmd+escape", ... }

So this can not be remapped or disabled by users via keybindings.json.

Source-level root cause

File: ~/.vscode/extensions/anthropic.claude-code-2.1.169-darwin-arm64/webview/index.js

After running the bundle through Prettier (--print-width 200), three handlers stand out:

1. The cancel-task listener — missing isComposing check

// ~ line 190685
document.body.addEventListener("keydown", (g) => {
  let b = l.activeSession.value;
  if (!b || g.defaultPrevented) return;
  let _ = document.querySelector(
    '.PermissionRequest_permissionRequestContainer[tabindex="0"]'
  );
  if (
    g.key === "Escape" &&
    !_ &&
    !g.altKey && !g.ctrlKey && !g.metaKey && !g.shiftKey
  ) (b.interrupt(), f());   //  ← interrupt() = cancel task
});

b.interrupt() resolves to:

// ~ line 129830
async interrupt() {
  if (!this.claudeChannelId || !this.connection.value) return;
  this.connection.value.interruptClaude(this.claudeChannelId);
}

2. The Rewind double-ESC handler — relies on a different guard (and is correct)

// ~ line 186354 (input field's React onKeyDown)
if ($.key === "Escape") {
  if (mn || Uo || T || te || he) {
    $.preventDefault(); A(!1); Q(!1); W(!1); ye(void 0); Me.current = 0;
    return;
  }
  if (b === "" && f) {                  // ← only counts the double-tap when input is empty
    let Le = Date.now();
    if (Le - Me.current <= Mn) {
      $.preventDefault(); Me.current = 0; f();   // ← open Rewind picker
      return;
    }
    Me.current = Le;
  }
  return;
}

This handler does not check isComposing, but it's effectively guarded by the b === "" (empty-input) precondition: while the IME is composing, the input is never empty, so the first ESC pressed during composition never increments the double-tap counter. The user-observable result is correct: ESC #1 dismisses the IME, ESC #2 + ESC #3 open the Rewind picker.

3. The Enter handler in the same function — checks isComposing directly

// ~ line 186385  (right after the Escape handler above)
if ($.key === "Enter" && !$.shiftKey) {
  if ($.nativeEvent.isComposing) return;   // ← correct guard
  ...
}

Side-by-side comparison

| Handler | Location | IME guard | Behavior with IME open |
|-------------------|--------------------------------------------|--------------------------------|--------------------------------------------------------------------------------------|
| Cancel task | document.body keydown listener (~190689) | ❌ none | Bug: cancels task on the first ESC, while the user only meant to dismiss the IME |
| Rewind double-ESC | input field React onKeyDown (~186354) | ✅ via b === "" precondition | Correct: 3 presses (1 dismisses IME, 2+3 open Rewind) |
| Enter (submit) | input field React onKeyDown (~186385) | ✅ nativeEvent.isComposing | Correct |

The codebase clearly knows how to handle ESC/Enter during IME composition — both the explicit isComposing check (Enter) and the empty-input precondition (Rewind) prevent the first ESC from misfiring. The cancel-task listener at line 190689 is the only place where neither protection exists, which is why it is the only handler that misbehaves with CJK IMEs.

The Enter handler is literally a few lines below the Escape handler in the same function. The fix should align Escape behavior with Enter.

A user-observable secondary symptom that confirms the model

  • With no task running and no IME: pressing ESC twice opens the Rewind to… picker.
  • With no task running but the IME candidate window open: it takes three ESC presses to open the Rewind picker.
  • With a task running and the IME candidate window open: a single ESC closes the IME and cancels the task.

The asymmetry (1 vs. 3) is exactly what the source predicts: the cancel-task listener on document.body does not gate on isComposing, while the Rewind double-tap requires b === "" and so consumes the first IME-related ESC silently.

Proposed minimal patch (1 line)

Line numbers and variable names below are taken from the published bundle (webview/index.js inside the v2.1.169 .vsix) after running it through Prettier. The original source layout is internal to Anthropic; the patch is offered as a structural suggestion rather than a literal diff.
 // line ~190689 (cancel-task listener)
 document.body.addEventListener("keydown", (g) => {
   let b = l.activeSession.value;
   if (!b || g.defaultPrevented) return;
+  if (g.isComposing) return;
   let _ = document.querySelector('.PermissionRequest_permissionRequestContainer[tabindex="0"]');
   if (g.key === "Escape" && !_ && !g.altKey && !g.ctrlKey && !g.metaKey && !g.shiftKey)
     (b.interrupt(), f());
 });

This brings the cancel-task listener in line with the Enter handler at line ~186385, which already uses the same guard.

Why this matters

  • Chinese / Japanese / Korean users naturally press ESC dozens of times a day to dismiss IME candidate windows.
  • Each accidental press silently destroys the in-flight Claude task and any pending tool calls, costing real time and tokens.
  • The fix is a single if (event.isComposing) return; line, with a precedent (the Enter handler) right next to it.

Related issues

  • #41772 — same symptom, closed as not planned. This issue adds runtime + source-level evidence and a concrete patch, so I'd kindly ask the team to reconsider.
  • #15269 — earlier IME ESC issue (resolved Feb 2026), but for the input submission path, not the task cancellation path covered here.
  • #47962 — IME pre-edit buffer cleared by UI redraws (different code path).

Thank you for considering this!

What Should Happen?

ESC pressed during IME composition should only be consumed by the IME (close the candidate window / cancel composition) and should not interrupt the running task — consistent with how the Enter key in the same input field already correctly checks isComposing.

Error Messages/Logs

Steps to Reproduce

  1. Start any Claude task that takes a few seconds (e.g., ask it to read a large file).
  2. While the task is running, type a few letters with a Chinese IME so that the candidate window appears.
  3. Notice you typed the wrong pinyin and press ESC to dismiss the candidate window.
  4. Bug: the running Claude task is interrupted at the same time.

Claude Model

None

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

1.1.7

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

VS Code integrated terminal

Additional Information

_No response_

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗