VSCode extension: add option to prevent panel from stealing focus

Status Open
Maintainer reply None cached
Activity 15 comments · opened Mar 10, 2026

Problem

When using Claude Code as a VSCode extension, the panel auto-reveals and steals focus whenever it produces output. This interrupts workflow in other editor tabs — for example, if I'm typing in a file and Claude finishes a response, focus jumps to the Claude panel.

Expected behavior

The extension should have an option to not steal focus when producing output. The user should be able to check Claude's output on their own terms.

Suggested solution

Add a preserveFocus setting (e.g., claude-code.preserveFocus: true) that prevents the extension panel from auto-revealing or grabbing focus when new output arrives. VSCode's WebviewPanel API supports preserveFocus natively, so this should be straightforward to wire up.

Workarounds

Currently the only workarounds are:

  • Running Claude in terminal mode (claude-code.useTerminal: true)
  • Running claude directly in the integrated terminal

Neither gives the full panel experience without the focus interruption.

View original on GitHub ↗

15 Comments

Ashkaan · 5 months ago

Just to clarify for anyone landing here — the notification hook doesn't stop the panel from stealing focus. It gives you a notification when Claude is waiting for input, but the WebviewPanel still calls reveal() without preserveFocus: true, so your editor focus still gets yanked every time there's output.

The actual fix needs to happen in the extension itself — passing preserveFocus: true to WebviewPanel.reveal(). The hook is a nice complement (get notified without watching the panel), but it's not a solution to the focus-stealing issue described in this ticket.

ahjwang · 5 months ago

Running into this too, specifically when using View: Toggle Maximize Panel — the Claude Code webview grabs focus instead of the panel maximizing. The issue has been reported in #14995 but it seems like there was no actual fix. Hopefully addressing this issue will also fix the focus stealing from panel maximizing. Many thanks to all involved!

ankursinghchawla · 5 months ago

+1. Running Claude Code in Cursor with "ask before edits" mode. The diff tabs steal focus from the active editor, which is highly disruptive. The preserveFocus fix would make a huge difference — please prioritize this.

landon-homeriz · 4 months ago

Related variant: when Claude Code calls MCP server tools (e.g., mcp__jcodemunch__search_text), each tool result opens as a new readonly editor tab (\temp\readonly\mcp__jcodemunch__search_text tool output (hpsr5d)). These tabs steal focus from whatever file you're working in.

This is especially disruptive when using MCP servers for code navigation — a single task can trigger 5-10 tool calls, each one opening a new tab over your active editor. The tabs are temporary/readonly and serve no purpose after Claude processes the result.

A preserveFocus setting would help, but ideally MCP tool output tabs shouldn't open as editor tabs at all — or at minimum there should be a setting to suppress them. The tool results are consumed by Claude, not the user.

Environment: Claude Code VS Code extension, Windows 11, multiple MCP servers (jCodemunch, jDocmunch, jDatamunch)

anka-213 · 4 months ago

Yes! This is very frustrating. I often press enter or escape for some unrelated reason and accidentally accept or reject claude's suggestion because of this.

collinmccarthy · 4 months ago

This is very annoying and potentially dangerous if you're pressing enter when something pops up. Please give us the option to disable this.

adam-aido · 3 months ago

That is really annoying. We still code by ourselves; Claude Code should not steal focus.

duniaka · 3 months ago

The same behavior is in PyCharm 2025.2.4. It is so annoying when i type the next prompt to the queue, the spacebar auto-confirms something. It would be good if I could disable this as well.

marketingmaniacs · 2 months ago

Confirmed in the code: the plan panel preserves focus, but the session tab doesn't.

Running multiple sessions, when one session presents a plan I get yanked to that session's tab while typing in another. I dug into the extension (v2.1.167, extension.js):

  • The plan preview webview is created correctly with preserveFocus: true: createWebviewPanel("claudePlanPreview", title, { viewColumn, preserveFocus: true }, ...) — so the plan panel itself does not steal focus.
  • But all three reveal() calls in the extension are made without the preserveFocus argument, so they default to taking focus. One of them is the callback handed to the session controller that fires when a session wants attention, which is what pulls me away.

The fix looks small: pass preserveFocus: true to those reveal(viewColumn, preserveFocus) calls (or gate it behind a setting). The blue/orange status dot is already enough of a signal; I'd like to decide myself when to visit a session instead of being pulled to it.

KylePinner · 2 months ago

Adding a multi-window use case for this. I run several Claude Code sessions at once, each in its own VS Code window (Windows 11). When I'm typing a prompt in one window and a session in another window proposes an edit, the diff opens and pulls focus away from where I'm working, so I lose my place and the edited file jumps on top of everything else. A preserveFocus option as suggested here would fix the worst of it: let the diff/output appear without raising the window or grabbing focus, so I can review on my own terms. Strong +1.

amalic · 1 month ago

Hitting this constantly in the VS Code extension, and the impact is bigger than "annoying": it makes it impossible to do anything else while Claude is working.

The whole point of a long-running agent task is to use that time - edit a file, write notes, answer something in another window. With the panel grabbing focus on every output, that time is unusable: you get pulled back mid-keystroke, repeatedly, for the entire duration of the task.

claude-code.preserveFocus as proposed here would fix it. Auto-reveal on the first output would be a fine default; re-stealing focus on every subsequent chunk is what makes it unworkable.

Env: Claude Code 2.1.217, VS Code 1.130.0.

xploSEoF · 1 month ago

Some findings from digging through the shipped bundle (anthropic.claude-code-2.1.220), which I think narrow this down usefully. There are two independent focus steals, the diff one is already fixed upstream, and the remaining one is in the webview and is deliberate.

The diff paths already preserve focus

Both diff-opening call sites in 2.1.220 already pass preserveFocus: true:

$ = { preview: false, preserveFocus: true }
await vscode.commands.executeCommand("vscode.diff", f, g, p, $)

x = { preview: false, preserveFocus: true }
await vscode.window.showTextDocument(d, x)

So "pass preserveFocus when opening the diff" is done. Anyone still seeing focus jumps is hitting one of the two paths below.

Steal 1 — openFile (extension host)

The MCP openFile handler computes preserveFocus from a parameter that defaults to taking focus:

async function openFile({ filePath, preview, startText, endText, selectToEndOfLine, makeFrontmost = true }) {
  …
  if (makeFrontmost || !alreadyVisible)
    editor = await vscode.window.showTextDocument(doc, { preview, preserveFocus: !makeFrontmost });

With makeFrontmost defaulting to true, preserveFocus evaluates to false — focus is taken deliberately, and there's no way to opt out short of the caller passing makeFrontmost: false.

Steal 2 — the permission prompt (webview)

This is the one that survives after fixing the above, and I think it's the bigger cause of reports in this thread: it fires on the approval path, so acceptEdits / bypassPermissions aren't workarounds for anyone who wants to keep reviewing changes.

In webview/index.js, the permission-request component schedules this 500ms after mount:

setTimeout(async () => {
  flushSync(() => { setPending(false) });
  if (isTextEntry(document.activeElement)) return;
  let isEditTool = e.toolName === "Edit" || e.toolName === "Write";
  if (f.current && document.hasFocus()) setIndex(0), t.safeFocus(f.current);
  else if (y.current && isEditTool) t.safeFocus(y.current);
}, 500)

f is the primary Yes button, y is the permissionRequestContainer div (tabIndex: 0).

The first branch is well-behaved — it only focuses the button when the webview already has focus. The else if is the problem: it runs only when document.hasFocus() is false, which is exactly the case where the user is typing in an editor or terminal. So an Edit/Write approval pulls focus out of wherever you were working, half a second after it appears — long enough that you're mid-keystroke when it lands.

The existing isTextEntry bail doesn't help, because it inspects the webview's document.activeElement. When focus is in an editor that's body, not an input.

The choke point is safeFocus, which every focus call in the app routes through:

safeFocus(e) { if (this.comms.connection.value?.isVisible.value) e.focus() }

It guards on the panel being visible, never on it being focused — so a visible-but-unfocused panel can still pull focus at will. Same applies to the terminal-kickback dialog, which calls safeFocus unconditionally on mount.

Proposed change

A claudeCode.preserveFocus boolean (default false, so current behaviour is unchanged), gating both steals:

  1. openFile — gate the makeFrontmost default so showTextDocument receives preserveFocus: true.
  2. safeFocus — add a focus guard at the choke point:
safeFocus(e) {
  if (!this.comms.connection.value?.isVisible.value) return;
  if (this.preserveFocus && !document.hasFocus()) return;
  e.focus();
}

The document.hasFocus() condition is what makes this safe to apply at the choke point rather than per-call-site: focus moves the user initiated inside the panel happen while the panel has focus and still work, while every cross-window grab is suppressed. The permission prompt keeps its keyboard flow — the f.current && document.hasFocus() branch is unaffected — and only the else if fallback goes quiet.

The one behaviour worth checking is panel open: if the webview's mount effects run before VS Code's focus lands, the composer may come up unfocused. If that races in practice, dropping the else if branch alone fixes the reported issue with no such risk, just less coverage of the other paths.

Note the settings namespace is camelCase in package.json (claudeCode.autosave, claudeCode.preferredLocation, …), so claudeCode.preserveFocus rather than claude-code.preserveFocus.

Related: tab accumulation

Separately, both diff call sites pass preview: false, which is why diff tabs open as permanent tabs and pile up rather than reusing the preview slot. That's the complaint in #25018, #52832 and #59820, and preview: true (or the same setting) would address it independently of the focus issue.

Temporary fix

For anyone who wants this today, both conditions can be forced in the installed bundle. This is a local hack — it's overwritten by every extension update, and the minified identifiers may shift between versions, so the script below verifies each pattern is present exactly once and stops if not.

EXT=$(ls -d ~/.vscode/extensions/anthropic.claude-code-*/ | tail -1)
echo "Patching $EXT"

HOST="$EXT/extension.js"
WEB="$EXT/webview/index.js"

OLD_HOST='preserveFocus:!o'
OLD_WEB='safeFocus(e){if(this.comms.connection.value?.isVisible.value)e.focus()}'
NEW_WEB='safeFocus(e){if(this.comms.connection.value?.isVisible.value&&document.hasFocus())e.focus()}'

# Verify both patterns are present exactly once before touching anything
for pair in "$HOST|$OLD_HOST" "$WEB|$OLD_WEB"; do
    f="${pair%%|*}"; pat="${pair#*|}"
    n=$(grep -o -F "$pat" "$f" | wc -l)
    [ "$n" -eq 1 ] || { echo "ABORT: found $n matches (expected 1) in $f — your version differs"; return 2>/dev/null || exit 1; }
done

cp -n "$HOST" "$HOST.backup"
cp -n "$WEB" "$WEB.backup"

perl -pi -e 's/\Qpreserve\EFocus:!o/preserveFocus:!0/' "$HOST"
perl -pi -e "s/\Q$OLD_WEB\E/$NEW_WEB/" "$WEB"

# Verify: both should print 0, and the host bundle should still parse
grep -c -F "$OLD_HOST" "$HOST"
grep -c -F "$OLD_WEB" "$WEB"
node --check "$HOST" && echo "host OK"

Then run Developer: Reload Window for the extension host and webview to pick it up.

To undo:

EXT=$(ls -d ~/.vscode/extensions/anthropic.claude-code-*/ | tail -1)
cp "$EXT/extension.js.backup" "$EXT/extension.js"
cp "$EXT/webview/index.js.backup" "$EXT/webview/index.js"

and reload, or reinstall the extension.

On Windows the extensions directory is %USERPROFILE%\.vscode\extensions instead.

The host edit is a single character (!o!0); the webview edit adds one &&document.hasFocus() clause. Confirmed working on 2.1.220 — happy to test a proper fix against a pre-release if that's useful.

Note: fix produced by Claude (Opus 5.0), and verified personally.

barsikus007 · 1 month ago

Following up on the 2.1.220 patch above — it fixes the diff/openFile steal, but safeFocus is not the only choke point, so the panel still pulls focus on some paths. On anthropic.claude-code-2.1.220 I count ~15 raw .focus() calls in webview/index.js that never go through safeFocus. The one that reproduces most reliably is the AskUserQuestion component, which focuses the first option on mount:

de(() => {
  let k = f.current;
  if (!k) return;
  k.querySelector('[role="radio"], [role="checkbox"]')?.focus()
}, [n]);

Others on the same footing:

de(() => { if (e.permissionRequests.value.length === 0) setTimeout(() => { a.current?.focus() }, 100) },
   [e.permissionRequests.value.length]);          // composer, when a request resolves (and on mount)
de(() => { a.current?.focus() }, [e.sessionId.value]);   // composer, on session switch
de(() => { if (r && l.current) l.current.focus() }, [r, l]);   // autoFocus inline input ("Other" in questions)

plus the modal component, which auto-focuses its primary button on open (h.focus() / p.focus() / a.current?.focus()).

Guarding each call site is a losing game, so the local fix below moves the guard one level down — to HTMLElement.prototype.focus itself. When the webview document isn't focused, the focus call isn't dropped, it's deferred: the element is remembered and focused once the panel actually receives focus. That avoids the panel-open race mentioned above (composer coming up unfocused), while every cross-pane grab goes quiet. It also subsumes the safeFocus edit — that one becomes redundant, though harmless if you already applied it.

The host-side openFile edit is still needed (different process), so it's included and is idempotent.

Worth noting what I deliberately left alone: ()=>e.show() (×2, the claudeVSCodeSidebar / claudeVSCodeSidebarSecondary providers) and ()=>e.reveal() in extension.js are also missing preserveFocus, but they're wired to makeVisible, which only fires when you click a notification button — being taken to the panel there is the point.

Same caveat as before: this is a local hack, overwritten by every extension update, and the minified identifiers shift between versions, so the script verifies before touching anything and is safe to re-run.

EXT=$(ls --directory ~/.vscode/extensions/anthropic.claude-code-*/ | tail --lines=1)
echo "Patching $EXT"

HOST="$EXT/extension.js"
WEB="$EXT/webview/index.js"

OLD_HOST='preserveFocus:!o'
GUARD='/* CLAUDE_FOCUS_GUARD */(()=>{let n=HTMLElement.prototype.focus,p=null,f=()=>{let q=p;p=null;if(q&&q.el.isConnected)n.apply(q.el,q.args)};window.addEventListener("focus",f);HTMLElement.prototype.focus=function(...a){if(document.hasFocus())return n.apply(this,a);p={el:this,args:a}}})();'

# extension host: openFile, makeFrontmost default
n=$(grep --only-matching --fixed-strings "$OLD_HOST" "$HOST" | wc --lines)
if [ "$n" -eq 1 ]; then
    cp --no-clobber "$HOST" "$HOST.backup"
    perl -pi -e 's/\Qpreserve\EFocus:!o/preserveFocus:!0/' "$HOST"
    node --check "$HOST" && echo "host patched"
elif [ "$n" -eq 0 ]; then
    echo "host: already patched (or version differs), skipping"
else
    echo "ABORT: found $n host matches (expected 0 or 1) — your version differs"
    return 2>/dev/null || exit 1
fi

# webview: guard every programmatic focus, defer instead of stealing
if grep --quiet --fixed-strings 'CLAUDE_FOCUS_GUARD' "$WEB"; then
    echo "webview: guard already present, skipping"
else
    cp --no-clobber "$WEB" "$WEB.backup"
    printf '%s\n' "$GUARD" | cat - "$WEB" > "$WEB.new" && mv --force "$WEB.new" "$WEB"
    node --check "$WEB" && echo "webview patched"
fi

Then run Developer: Reload Window so both the extension host and the webview pick it up.

To undo:

EXT=$(ls --directory ~/.vscode/extensions/anthropic.claude-code-*/ | tail --lines=1)
cp --force "$EXT/extension.js.backup" "$EXT/extension.js"
cp --force "$EXT/webview/index.js.backup" "$EXT/webview/index.js"

and reload, or reinstall the extension. On Windows the extensions directory is %USERPROFILE%\.vscode\extensions.

Confirmed on 2.1.220 (Linux, VS Code): with this in place, an AskUserQuestion prompt arriving while I'm typing in an editor no longer moves the caret — the panel renders the question and the option gets focused only when I switch to the panel myself.

junkerderprovinz · 12 days ago

Also affects the AskUserQuestion popup specifically: if you're typing in another editor tab when Claude shows a clarifying-question dialog, focus jumps straight to the popup and interrupts typing mid-word. Same root cause as described above (WebviewPanel.reveal() without preserveFocus: true), just a different trigger than every-output-reveal. Would love to see this land.

amalic · 9 days ago
Also affects the AskUserQuestion popup specifically: if you're typing in another editor tab when Claude shows a clarifying-question dialog, focus jumps straight to the popup and interrupts typing mid-word. Same root cause as described above (WebviewPanel.reveal() without preserveFocus: true), just a different trigger than every-output-reveal. Would love to see this land.

Even worse, a key press accepts the first pre-selected option before gou can even read the question.