Skip to content

Usage window opens blank and inline bars freeze after the first scrape - WebView2 destroyed on frame.Hide() and never re-initialized (regression since v10.31) #131

Description

@metman-oss

Hi Daniel,

I had a problem with Claude Code's usage display and had it run a diagnostic check on its own. I haven't checked it yet, but the symptom describes what I've observed.

Greetings
René

Summary

Two symptoms, one cause:

  1. The inline usage bars only ever show the value scraped once at startup; every subsequent
    background refresh is a silent no-op.
  2. Opening the Claude Usage tool window after that startup cycle shows a completely blank
    panel
    — no page, not even the "Loading…" hint.

Both happen because VS tears the WebView2 down as soon as the usage tool window frame is
hidden at the end of the startup show/hide cycle, and nothing ever re-initializes it:
Reload() and Navigate() are then called on a CoreWebView2 that no longer exists.

This is the real cause behind #111 (closed as fixed in v75.0). v75.0 only restored the
startup scrape, so after a restart it looks fixed for a moment and then freezes again —
which matches the reporter's "everything is working fine now" right after updating.

Environment

  • Extension v139.0 (commit d455399, clean checkout of master, no local changes)
  • Visual Studio 18.8.12023.21 (stable)
  • Windows 11 Pro 10.0.26200
  • WebView2 Runtime 151.0.4129.72
  • Provider: Claude Code, ShowInlineUsageBars: true, UsageAutoRefreshSeconds: 120,
    UsageWindowOpened: false (inline bars only, tool window closed)

Evidence (from a live session, 9 minutes after VS start)

Measurement Value
devenv started 08:18:06
LastUsageTimestamp in claudecode-settings.json 08:18:25 — the single startup scrape
Time of measurement 08:27 → ~4 timer ticks should have happened at 120 s
WebView2 processes with --user-data-dir=…\ClaudeCodeExtension\WebView2 none
Last write anywhere in the WebView2 profile (Cookies, History, Network State) 08:18:25, nothing after
EBWebView\lockfile absent → browser process shut down cleanly
Crashpad dumps none → no crash, an orderly teardown
Opening the usage tool window afterwards blank panel, confirmed

So the WebView2 is fully gone the moment frame.Hide() runs at the end of
EnsureUsageToolWindowAsync's background-init branch: VS removes the tool window content
from the visual tree, the HwndHost destroys the hosting window, and CoreWebView2 dies
with it.

Root cause

Controls/ClaudeCodeControl.Usage.cs:280 short-circuits every refresh:

if (_usageToolWindow.UsageControl?.IsWebViewInitialized == true)
{
    _backgroundScrapeCompletionTcs = new TaskCompletionSource<bool>();
    _usageToolWindow.UsageControl?.Reload();          // no-op: CoreWebView2 is gone
    await Task.WhenAny(_backgroundScrapeCompletionTcs.Task, Task.Delay(10000)); // times out
    _backgroundScrapeCompletionTcs = null;
    _usageToolWindow.UsageControl?.MarkNeedsReloadOnShow();
    return;                                           // working fallback never reached
}

Two things combine:

  1. IsWebViewInitialized => _initialized (UI/ClaudeUsageControl.xaml.cs:651) is only an
    "OnLoaded has run once" flag, not a liveness check. It stays true forever, long after
    CoreWebView2 has been destroyed.
  2. Reload() (UI/ClaudeUsageControl.xaml.cs:919) is
    try { WebView?.CoreWebView2?.Reload(); } catch { } — with a dead CoreWebView2 nothing
    happens and nothing is reported.

Result: the working fallback (ShowNoActivate → scrape → Hide, line 295 ff.) is never
entered again after the first startup cycle. The same no-op also swallows the control's own
auto-refresh timer (RestartAutoRefreshTimerTick += Reload,
UI/ClaudeUsageControl.xaml.cs:909), which is why the Auto-refresh setting makes no
difference either.

Regression

Commit 27bf92a"10.31 - Usage tab no longer blinks during background refresh". Before
that, every refresh did the full show/hide cycle, which still works today (it produces the
one startup value). It was replaced by a hidden Reload() with the assumption in the
comment that "CoreWebView2 processes navigation and JS messaging even when the frame is
hidden"
— true only as long as the WebView2 is still alive, which it isn't after Hide().

Repro

  1. Claude Code provider, inline usage bars on, usage tool window closed, auto-refresh on (120 s).

  2. Start VS, wait for the bars to appear (startup scrape works).

  3. Wait 5–10 minutes, then look at LastUsageTimestamp in
    %LOCALAPPDATA%\ClaudeCodeExtension\claudecode-settings.json — it never advances.

  4. Get-CimInstance Win32_Process -Filter "Name='msedgewebview2.exe'" shows no process with
    the extension's --user-data-dir.

  5. Now open the Claude Usage tool window → blank panel (confirmed). OnLoaded bails out
    on if (_initialized) return; so InitializeWebViewAsync() never runs again,
    OnWindowBecameVisible navigates a null CoreWebView2, and LoadingText was already
    set to Collapsed by the first NavigationCompleted — hence not even a loading hint.

Workaround for users until this is fixed: toggle the Show Usage toolbar button off and on.
ForceClose() destroys the tool window, so the next FindToolWindow(create: true) builds a
fresh ClaudeUsageControl with a working WebView2.

Suggested fix

  1. Replace the IsWebViewInitialized gate with a real liveness check
    (WebView?.CoreWebView2 != null) and only take the reload short-circuit when it holds.
  2. When CoreWebView2 is dead, run the startup show/hide cycle again including re-init.
    Since the blank-window symptom shows the existing WebView2 element cannot be revived
    after being torn out of the visual tree, this needs a fresh WebView2 instance placed
    into a host container (rather than the fixed x:Name="WebView" element), plus resetting
    _initialized so InitializeWebViewAsync() runs again. That also fixes the blank panel
    on explicit open.
  3. Let Reload() return bool so the host notices a failed reload immediately, and force a
    full cycle on the next tick when the scrape wait times out (self-healing).
  4. Tab blinking — the reason for v10.31 — should be milder than back then, because the
    fallback path now uses ShowNoActivate() instead of Show().

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions