[BUG] Multiple visible Claude panels fight over focus: typing lands in the wrong panel, high CPU
Focus ping-pong between multiple Claude panels: typing lands in the wrong panel, CPU spikes
Summary
When two or more Claude Code panels are simultaneously visible in different editor
groups (side by side), clicking on the VS Code window makes focus flicker between the
panels. Typed characters land in a different panel than the one that appears focused, and
CPU usage climbs sharply. The only way out is to click outside the VS Code window and back
in, repeatedly, until the race happens to settle.
This is fully reproducible and does not require any unusual configuration — only having
more than one Claude panel visible at once.
Environment
| | |
|---|---|
| Extension | anthropic.claude-code 2.1.231 (linux-x64) |
| VS Code | 1.133.0, commit a5b500951314efd502d07465bd138dfbd714a960, x64 |
| OS | Ubuntu 26.04 LTS, GNOME Shell 50.1 |
| Display server | Wayland (native) — renderer runs with --ozone-platform=wayland |
| GPU | AMD Radeon (Mesa 26.0.3), gpu_compositing: enabled, rasterization: enabled |
| Hardware | AMD Ryzen 7 7735HS, 30 GB RAM |
| claudeCode.preferredLocation | panel |
Note: 2.1.231 was the latest published version at the time of this report, so this is not
fixed by updating.
Steps to reproduce
- Set
claudeCode.preferredLocationtopanel. - Open a Claude Code session in an editor tab.
- Split the editor and open a second Claude Code session in the new group, so that two
Claude panels are visible at the same time. (The effect gets worse with three.)
- Click on some other application, then click back on the VS Code window.
- Start typing.
Expected behavior
Focus goes to exactly one panel — the active one — and typed text goes to that panel.
Actual behavior
- Focus visibly flickers between the Claude panels.
- Typed text is delivered to a panel other than the one that appears active.
- CPU usage rises sharply and stays high; the webview renderer processes are the top
consumers.
- Clicking away from the window and back, several times, eventually settles it.
Root cause analysis
I disassembled the shipped bundles. There are two contributing mechanisms.
1. Every visible webview re-focuses its own input on window focus
In webview/index.js, each webview instance registers a focus listener on window:
let C = () => {
setTimeout(() => {
let x = document.activeElement,
y = ni.messagesContainer;
if (!x || x === document.body || (y !== void 0 && x.classList.contains(y)))
i.emit(""); // → re-focuses the prompt input
}, 0);
};
window.addEventListener("focus", C);
Each Claude panel is a separate iframe. When the VS Code window gains focus, the focus
event fires in every visible iframe, not only the active one. Inside an iframe that
holds no internal focus, document.activeElement is document.body — so the guard
condition is satisfied in all visible panels at once, and each one re-focuses its own
prompt input. They then steal focus from one another.
The guard is intended to mean "nothing specific is focused here, so restore the input", butdocument.activeElement === document.body is not a reliable proxy for "this iframe is the
one the user is interacting with".
A related cleanup effect re-focuses a fallback element under a similar condition:
function Dit(e, t) {
Mh(() => {
let i = e.current;
return () => {
if (i !== null && i.contains(document.activeElement) && document.hasFocus())
t?.current?.focus({ preventScroll: !0 });
};
}, [e, t]);
}
2. A single global activeSessionId, broadcast to every webview
In extension.js, all panels share the same viewType (claudeVSCodePanel), and the host
tracks one active session id globally:
setActivePanel(e) {
for (let [t, r] of this.sessionPanels)
if (r === e) { this.activeSessionId = t; this.broadcastSessionStates(); return; }
}
broadcastSessionStates() {
let e = Array.from(this.sessionStates.values()),
t = Array.from(this.sessionPanels.keys());
for (let r of this.allComms) r.sendSessionStates(e, this.activeSessionId, t);
// ...
}
Every panel's onDidChangeViewState handler triggers three things:
e.onDidChangeViewState(() => {
if (!n && e.viewColumn !== void 0) l = e.viewColumn;
a.notifyVisibilityChange(e.visible);
this.updateSidebarActiveState();
if (e.active) this.setActivePanel(e);
}, null, this.disposables);
A single focus change flips the view state of several panels (one becomes active, the
others stop being active). Each of those fires broadcastSessionStates(), which posts a
message to all comms rather than to the panel that actually changed. With N panels the
message volume per focus event is O(N²), and each message re-renders every webview.
This is the CPU component, and it also explains the "typed text goes to the wrong panel"
symptom: the keyboard goes to whichever panel won the focus race in mechanism 1, while the
globally-tracked activeSessionId may point at a different one.
3. Aggravating factor: nothing is ever unloaded
All Claude webviews are created with retainContextWhenHidden: true —claudeVSCodePanel, both sidebar providers, the sessions list, and the plan preview:
Dt.window.createWebviewPanel("claudeVSCodePanel", "Claude Code", i, {
enableScripts: !0,
retainContextWhenHidden: !0,
enableFindWidget: !0,
localResourceRoots: [ /* ... */ ]
});
So no panel is ever discarded; every session keeps a live iframe holding its full
transcript. With several concurrent sessions the re-render storm from mechanism 2 is paid
by all of them at once.
Related issues
- #77696 —
Cmd+Nbroadcasts new-conversation to all open Claude tabs. Same
architectural pattern: a per-panel action fanned out over this.allComms instead of being
routed to the active panel. The fix suggested there (route to the focused panel rather
than iterating allComms) would also address mechanism 2 below.
- #74808 — Inserting an @-mention steals focus to the Claude editor tab. Neighbouring
focus-routing defect in the same extension.
I could not find an existing report for the flicker/wrong-panel symptom itself.
Impact
With several concurrent sessions open side by side, the extension becomes difficult to use:
prompts get typed into the wrong session, and the machine heats up. Because the panels each
map to a separate claude process, this is a normal setup for anyone running more than one
task at a time.
Suggested fix
The narrow fix is mechanism 1: gate the window focus handler on the panel actually being
the active one, rather than on document.activeElement === document.body. The host already
knows which panel is active (setActivePanel / activeSessionId) and already pushesvisibility_changed to the webview — the same channel could carry an isActive flag that
the focus handler checks before re-focusing.
Two smaller improvements alongside it:
- Make
broadcastSessionStates()target the panel whose state changed, instead of
iterating allComms on every view-state transition.
- Consider not setting
retainContextWhenHidden: trueunconditionally, or capping how many
hidden sessions stay resident.
Workaround
Keep only one Claude panel visible per window — stack the sessions as tabs within a single
editor group instead of splitting them side by side. Hidden iframes do not receive thewindow focus event, so the race disappears. Sessions that must be watched at the same time
can be placed in separate VS Code windows.