Skip to content

Fix webview/status bar getting stuck on "Loading…" - #20

Merged
Carl723000 merged 1 commit into
ClaudeCodeUsage:mainfrom
nickearnshaw:nickearnshaw/fix-loading-stuck
Jun 15, 2026
Merged

Carl723000 merged 1 commit into
ClaudeCodeUsage:mainfrom
nickearnshaw:nickearnshaw/fix-loading-stuck

Conversation

@nickearnshaw

Copy link
Copy Markdown
Contributor

Problem

During active Claude Code use the usage panel (and the status bar) often gets stuck showing the "Loading…" spinner and never renders the dashboard.

Root cause

refreshData() is triggered by both the auto-refresh timer (≥30s) and the file watcher (1.5s debounce, which fires repeatedly while Claude Code is writing logs). On every trigger:

  • setLoading(true) replaces the entire webview HTML with the spinner and flips the status bar to $(sync~spin) Loading….
  • refreshData() then re-reads and re-parses every JSONL file from scratch — seconds on a large history.
  • There is no re-entrancy guard, so overlapping reloads pile up and a fresh setLoading(true) keeps re-asserting the spinner around the time each slow load finishes → effectively perpetual "Loading…".

Fix

Two small, data-behaviour-preserving changes in src/extension.ts:

  1. Re-entrancy guard (isRefreshing): a trigger that arrives while a load is in flight is coalesced instead of starting a second full reload on top of the first.
  2. Spinner only on first load: the full-screen "Loading…" state is shown only when there is no data yet (cold cache). Background refreshes keep the existing dashboard / status-bar value visible and swap in fresh data when ready, via the existing updateData() / updateUsageData() calls.

Error and no-data paths are unchanged and still surface their messages.

Testing

  • npm run compile is clean.
  • Manually: open the panel and either repeatedly run Claude Code Usage: Refresh Usage Data or use Claude Code so logs keep changing. Before: panel flickers to / sticks on the spinner. After: the dashboard stays visible and updates in place; the spinner only appears on the very first open.

🤖 Generated with Claude Code

The auto-refresh timer and file watcher both call refreshData(), which
re-read every JSONL file from scratch. Each call replaced the whole
webview with a spinner and flipped the status bar to "Loading…". With no
re-entrancy guard, overlapping slow reloads during active Claude Code use
kept re-asserting the spinner, so the panel was effectively stuck loading.

- Add an isRefreshing guard so a trigger arriving mid-load is coalesced
  instead of starting a second full reload.
- Only show the full-screen spinner on the first load (empty cache).
  Background refreshes keep the existing dashboard/status bar visible and
  swap in fresh data when ready.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Carl723000 added a commit to Carl723000/ClaudeCodeUsage that referenced this pull request Jun 3, 2026
…ner, iOS toggle

Summary of all changes landing in the v2.0.1 release:

Fixes
- Dedup now keeps the higher-token record when hashes collide (issue ClaudeCodeUsage#18,
  @zhaoxiao9302). Proxy "placeholder + real" pairs are handled correctly.
- DeepSeek pricing: deepseek-chat and deepseek-reasoner corrected from
  Flash rates to Pro rates ($0.435/$0.87). Added deepseek-v4-pro entry.
- Quota indicator no longer hides when workspace has no local Claude data.
- Webview / status bar no longer gets stuck on "Loading…" (PR ClaudeCodeUsage#20,
  @nickearnshaw): re-entrancy guard + spinner only on cold start.
- Log timestamps now use local time (not UTC).

Added
- Dashboard "Auto-refresh" toggle (iOS-style) with localised label.
  When off: status bar still updates, webview pauses until "Refresh Now".
- claudeCodeUsage.fileWatching setting (opt-out fs.watch).
- claudeCodeUsage.pauseDashboardRefresh setting.
- Opus 4.8 explicit pricing entry.
- Diagnostic output channel: per-refresh stats + per-model record/token counts.

Docs
- README intro rewritten to "The Claude Code coach in your status bar"
  slogan (all language variants updated).
- New Troubleshooting entries: cleanupPeriodDays for history retention
  (PR ClaudeCodeUsage#21, @nickearnshaw) and sub-agent token under-counting note.
- Credits updated for PRs ClaudeCodeUsage#20, ClaudeCodeUsage#21, ClaudeCodeUsage#22 (@nickearnshaw).

Dev
- launch.json preLaunchTask fixed for single-root checkout (PR ClaudeCodeUsage#22,
  @nickearnshaw).
Carl723000 added a commit that referenced this pull request Jun 3, 2026
…#23)

* chore(pricing): add Opus 4.8 to the pricing table

Assumed at the current Opus tier rate ($5 / $25 input/output per MTok,
$6.25 cache-write, $0.5 cache-read), same as 4.7 / 4.6 / 4.5.
Family-aware fallback would already pick the Opus tier for unknown
opus-4-* snapshots, but an explicit entry is cleaner and keeps the
"Per-model rates" line in the dashboard accurate.

If Anthropic differentiates Opus 4.8 pricing later, override here or
just hit the "Refresh Token Pricing" button to pull live LiteLLM data.

* fix: keep the higher-token record on uniqueHash collision + correct DeepSeek pricing

Issue #18 (zhaoxiao9302): when a proxy like mimo or CC Switch writes
two records per message — a tokens=0 placeholder when streaming starts
and the real values after the response completes — both records share
the same messageId and lack a requestId, so they hash identically.
The previous dedup kept whichever appeared first, which was almost
always the zero-value placeholder, leading to massively under-reported
usage (the reporter measured \$0.20 displayed vs. \$7.98 actual on a
single day; 185 of 188 today-records had this pattern).

Fix: switch processedHashes from Set<string> to Map<string, number>
(hash → records[] index). On a duplicate hash, compare the new and
existing total token sums and keep whichever is higher. This naturally
resolves the placeholder/real pair regardless of arrival order, without
needing to detect the proxy itself.

DeepSeek pricing was also wrong: `deepseek-chat` and `deepseek-reasoner`
were priced at V4-Flash rates (\$0.14 / \$0.28 per MTok) even though
the `deepseek-chat` alias has pointed to V4-Pro since 2024-09. They are
now priced at the Pro tier (\$0.435 / \$0.87 / cache hit \$0.003625),
verified against https://api-docs.deepseek.com/quick_start/pricing/.
Added a separate `deepseek-v4-pro` entry. The family fallback for
unknown DeepSeek snapshots now also resolves to the Pro tier.

This is the same root cause behind the under-reported DeepSeek usage
seen by users running CC Switch.

Reported-by: @zhaoxiao9302 (issue #18)

* fix: decouple quota from local data + opt-out file watcher + shorter cache

Three related fixes for the quota indicator and the file watcher:

1. Quota now refreshes unconditionally at the start of every refresh
   cycle and is no longer hidden when the workspace lacks Claude history
   or when the data directory cannot be found. The quota is account-
   level, so a fresh workspace should not affect it.

2. Quota fetch cache shortened from 2 min to 30 s, so switching between
   VS Code windows no longer shows a stale percentage for up to two
   minutes.

3. New setting `claudeCodeUsage.fileWatching` (default: true) — when
   disabled, falls back to interval-based refresh only. Closes issue
   #17 (Rissew): constant refreshes during active token use can be
   distracting, this gives users the calmer mode on demand.

StatusBarManager.updateUsageData() no longer touches the quota item:
the caller (extension.refreshData) calls updateQuota() separately at
the very start of the cycle. This change is callback-compatible — the
optional usageLimits arg still works for callers wanting a single
update path.

Closes issue #17.

* feat: HTML+SVG progress-bar quota tooltip

Replace the Markdown table tooltip with an HTML <table> that embeds
inline SVG progress bars per row, mirroring what the official `/usage`
command shows. The bar fill colour changes by threshold to match the
status-bar warning/error backgrounds:

  - green  at  <80%   ("plenty left")
  - amber  at  >=80%  ("getting close")
  - red    at  >=95%  ("at the limit")

SVG was chosen over CSS-styled <div>s because VS Code's MarkdownString
sanitiser strips most `style` attributes but lets SVG through intact.
This means the bars render correctly on both light and dark themes
without needing theme-aware CSS variables.

The percentage and reset countdown columns are kept for users who
prefer the precise number; the weekly row still uses <br> to put the
reset moment and the countdown on two lines.

* fix: relax validator, restore 120s quota cache, HTML+span progress bars

Four small follow-ups from RC testing:

- Bump package.json to 2.0.1 so the packaged .vsix has the right name
  during the RC cycle.

- Quota cache: 30 s → 120 s. Multi-workspace sync was the goal of the
  shorter window, but 30 s feels too chatty during active token writes;
  120 s matches the polling interval.

- Relax validateUsageRecord. The previous version dropped any record
  whose secondary fields had an unexpected type (e.g. `model` is null,
  `requestId` is a number). That likely cost us records from new
  Claude Code features (xhigh / ultracode / workflow) and from some
  proxies that write slightly atypical JSON. Now only the two real
  must-haves are required: timestamp + numeric input/output tokens.
  All other fields are treated as best-effort.

  Added validationDropReason() and a stats counter so the loader writes
  a one-line "X files, Y lines, kept=N, dedup-replaced=…, rejected: …"
  summary to the "Claude Code Usage" output channel on every refresh.
  This lets us see exactly which validator rule is rejecting records
  rather than guessing.

- Quota tooltip progress bars: SVG was being stripped by VS Code's
  markdown sanitiser (so the new look never reached users). Switched
  to coloured <span>s with non-breaking-space padding — pure HTML,
  background-color is in the safe-CSS allowlist, renders consistently.
  Same colour thresholds as the status-bar background (green / amber
  at 80% / red at 95%).

* fix(tooltip): wider bar, 70% height, gray 100% frame, 24h weekly reset

Small polish on the quota tooltip from RC testing:

- Bar widened from 16 to 24 segments so low percentages (5h:24%) read
  proportionally instead of "barely a sliver".
- Bar height compressed via font-size:70% on the outer span so the bar
  looks like a thin strip rather than a full-height block.
- Outer span now has a gray 100%-width background — the "track" /
  frame is always visible regardless of fill, so users can see where
  the bar ends and where the boundary is.
- Weekly reset time switched to 24-hour format (hour12:false) to
  suppress AM/PM that some locales add. "Wed 03:00" stays "Wed 03:00".

* fix(tooltip): rounded corners, visible gray frame, 60% compression + diagnostics model breakdown

Tooltip polish (round 2):
- border-radius: 4px on both bar layers for rounded corners.
- Gray frame opacity bumped from 0.30 -> 0.55 so the 100% boundary is
  actually visible (previous version disappeared on most themes).
- font-size 70% -> 60% — bar reads as a thin horizontal strip rather
  than a full-line block.

Loader diagnostics now also list every model name found, sorted by
record count. This makes it instantly obvious whether a proxy is
writing records under a non-default model name (e.g. CC Switch's
deepseek-v4-pro[1m]) so we can investigate without guessing.

* fix(tooltip+diag): visible bar border + 48% height; log token-sum per model

Two follow-ups from RC testing round 4:

- Quota tooltip:
  - Bar height further compressed (font-size 60% → 48%) so the bar
    reads as a thin strip; height is now ~80% of what RC round 3 showed.
  - Added a 1px gray border on the outer span so the 0-100% frame is
    visible at all fill levels (the rgba track alone was easy to miss
    on busy backgrounds). Inner fill keeps 3px radius for soft corners.

- Loader diagnostic: model summary now reports token sum per model in
  addition to record count, formatted as e.g. `deepseek-v4-flash=99/0`.
  A model with N records but 0 total tokens is conclusive proof that
  every record for that model was a proxy zero-token placeholder — the
  pattern behind the missing-Flash story in CC Switch sub-agent paths.

* fix: visible tooltip frame, local-time logs, pause-dashboard-refresh setting

Three RC-round-5 improvements bundled as one meaningful commit (in line
with the "every commit should pull its weight" rule):

1. Quota tooltip progress bar:
   The previous outer-span + rgba border was invisible under both light
   and dark themes. Rebuilt as four adjacent solid-coloured spans:
   `[dark-gray bookend][filled cells][light-gray empty][dark-gray bookend]`.
   The dark-gray (#666) bookends mark 0% and 100% with strong contrast on
   any theme; the light-gray (#cccccc) empty fill keeps the unfilled range
   obvious. Bar colour by threshold (green / amber / red) unchanged.

2. Diagnostic log timestamps now use the local timezone instead of UTC
   (`toLocaleTimeString(undefined, { hour12: false })`). Affects both the
   loader and the API client output channel lines.

3. New `claudeCodeUsage.pauseDashboardRefresh` setting (default false).
   When on, fs.watch and the polling timer skip the webview update; the
   status bar still refreshes so today's cost and quota stay live. The
   "Refresh Usage Data" command always force-updates the webview so
   users can pull a fresh view on demand. Addresses the issue #17
   follow-up — agents writing to logs no longer thrash an open dashboard.

* feat(dashboard): in-page auto-refresh toggle; tooltip uses solid gray track

Two improvements in one commit:

1. Dashboard now has an "Auto-refresh: ON/OFF" toggle in the header.
   Default state mirrors the `claudeCodeUsage.pauseDashboardRefresh`
   setting. Clicking the toggle:
     - flips the CSS class on <body> (auto-off) so the conditional
       "Refresh Now" button appears in the header
     - persists the new state by updating the setting
   When OFF, the status bar still refreshes; the dashboard waits for
   the user to press "Refresh Now" or run the command. This addresses
   issue #17's follow-up comment — agents writing to logs no longer
   thrash an open dashboard while still keeping the user in control.

2. Quota tooltip progress bar reverted to the previously-rendering
   nested-span structure (the bookend approach didn't render in
   testing). Outer track is now solid #bbb instead of rgba — solid
   light-gray is visible on both light and dark themes without
   relying on opacity that some themes wash out. Inner colour fill
   sits on top with green/amber/red thresholds unchanged.

If the solid-gray track is still not visible enough, the path forward
is a v2.1 redesign rather than another small CSS tweak — this commit
is the last cosmetic attempt for v2.0.1.

* feat(dashboard): iOS-style auto-refresh toggle + i18n for label

Replace the previous text button with a proper switch:

- A native-looking <label> with an iOS-style slider (32x18 px). Slider
  turns green (#34c759) when auto-refresh is on, gray when off. Knob
  slides across via CSS transition. Focus ring uses the VS Code focus
  border colour.
- Switch state stays in sync with `claudeCodeUsage.pauseDashboardRefresh`
  and survives reload. The "Refresh Now" button only shows when the
  switch is off (controlled by the .auto-off body class).
- New i18n key `autoRefresh` translated for all six languages:
  EN "Auto refresh", DE "Auto-Aktualisierung", zh-TW "自動刷新",
  zh-CN "自动刷新", JA "自動更新", KO "자동 새로고침". Replaces the
  hardcoded English label.

Tooltip stays at 4e3f238 (nested-span with solid #bbb gray); further
cosmetic work deferred to v2.1.

* fix(v2.0.1): dedup, DeepSeek pricing, quota always-show, loading spinner, iOS toggle

Summary of all changes landing in the v2.0.1 release:

Fixes
- Dedup now keeps the higher-token record when hashes collide (issue #18,
  @zhaoxiao9302). Proxy "placeholder + real" pairs are handled correctly.
- DeepSeek pricing: deepseek-chat and deepseek-reasoner corrected from
  Flash rates to Pro rates ($0.435/$0.87). Added deepseek-v4-pro entry.
- Quota indicator no longer hides when workspace has no local Claude data.
- Webview / status bar no longer gets stuck on "Loading…" (PR #20,
  @nickearnshaw): re-entrancy guard + spinner only on cold start.
- Log timestamps now use local time (not UTC).

Added
- Dashboard "Auto-refresh" toggle (iOS-style) with localised label.
  When off: status bar still updates, webview pauses until "Refresh Now".
- claudeCodeUsage.fileWatching setting (opt-out fs.watch).
- claudeCodeUsage.pauseDashboardRefresh setting.
- Opus 4.8 explicit pricing entry.
- Diagnostic output channel: per-refresh stats + per-model record/token counts.

Docs
- README intro rewritten to "The Claude Code coach in your status bar"
  slogan (all language variants updated).
- New Troubleshooting entries: cleanupPeriodDays for history retention
  (PR #21, @nickearnshaw) and sub-agent token under-counting note.
- Credits updated for PRs #20, #21, #22 (@nickearnshaw).

Dev
- launch.json preLaunchTask fixed for single-root checkout (PR #22,
  @nickearnshaw).
@Carl723000

Copy link
Copy Markdown
Collaborator

Thanks @nickearnshaw — this landed in v2.0.1. Closing as released. Really appreciate the clear write-up.

@Carl723000 Carl723000 closed this Jun 8, 2026
@Carl723000 Carl723000 reopened this Jun 15, 2026
@Carl723000
Carl723000 merged commit 7a1aa40 into ClaudeCodeUsage:main Jun 15, 2026
1 check passed
@Carl723000

Copy link
Copy Markdown
Collaborator

This is now merged for real, @nickearnshaw — sorry it originally showed as "closed".

Thanks again!

@Carl723000 Carl723000 added the skip-changelog Exclude from auto-generated release notes label Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changelog Exclude from auto-generated release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants