#app has width:100vw and a 1px border with default content-box box-sizing, so its rendered size is 100vw+2px which overflowed body → unwanted scrollbars. |
||
|---|---|---|
| scripts | ||
| src | ||
| src-tauri | ||
| .gitignore | ||
| CLAUDE.md | ||
| index.html | ||
| memory.md | ||
| package.json | ||
| README.md | ||
| svelte.config.js | ||
| tsconfig.json | ||
| tsconfig.node.json | ||
| vite.config.ts | ||
claude-usage-widget
A small always-on-top Windows desktop widget that visualizes your local Claude Code usage:
- Current 5-hour session block — progress ring + countdown to block end.
- Past 7 days — daily bars + total.
- Per-model breakdown — Opus / Sonnet / Haiku stacked across the current block.
Reads ~/.claude/projects/**/*.jsonl directly. No Anthropic API. No auth.
┌────────────── Claude Usage ───────╳ ─┐
│ │
│ ╭─────╮ │
│ ╱ ╲ │
│ │ 47k │ ← 5-hour block │
│ │ 24% │ │
│ ╲ 3h 12m╱ │
│ ╰─────╯ │
│ │
│ Models (current block) │
│ ▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱ │
│ ● Opus 32k ● Sonnet 11k │
│ │
│ 7-day total 842k · 42% │
│ ▁▃▆█▂▅▇ │
│ Sat Sun Mon Tue Wed Thu Fri │
└───────────────────────────────────────┘
What this is not
It does not call the Anthropic API and cannot show server-side ground-truth caps. The "5-hour" and "weekly" numbers are derived from your local JSONL transcripts — the same data ccusage operates on. The cap values shown in the percentage and the warning thresholds are user-configurable in Settings.
Architecture (one paragraph)
A Tauri 2 app with a Rust backend and a Svelte 5 + Vite + TS frontend. The Rust side enumerates ~/.claude/projects/**/*.jsonl, tail-parses each file (resuming from a cached byte offset so we never re-parse already-seen lines), dedupes events by requestId || uuid (subagent transcripts overlap parents), aggregates into 5-hour blocks (ccusage-equivalent algorithm: block_start = floor_to_hour(first_ts), block_end = block_start + 5h, new block on ≥5h gap or end-of-block), computes a rolling 7-day window in the user's local timezone, and emits a usage-updated event whenever anything changes. A notify-debouncer-full watcher fires on file changes; a 60s tokio::time::interval poll backstops it because ReadDirectoryChangesW on the WSL \\wsl$\ 9P mount can miss events. The widget window is frameless, transparent, alwaysOnTop, skipTaskbar, and 280×360 px; drag via the custom title bar.
Build & run
You need a Windows host with the Tauri 2 toolchain — see Tauri prerequisites. Quick version:
winget install Rustlang.Rustup OpenJS.NodeJS.LTS
rustup default stable-x86_64-pc-windows-msvc
npm i -g pnpm
# Also: MSVC C++ Build Tools + Windows SDK (Visual Studio Installer),
# and Microsoft Edge WebView2 Runtime (preinstalled on Windows 11).
Then, from this directory:
pnpm install
pnpm tauri dev # iterate
pnpm tauri build # NSIS installer in src-tauri\target\release\bundle\nsis\
If you're developing in WSL but building on Windows, the WSL filesystem is mounted at \\wsl$\<distro>\ from Windows; clone or copy this folder onto the Windows side (or work directly via the UNC path) before running pnpm tauri build — Tauri itself needs the MSVC linker.
Icons. Before
pnpm tauri buildsucceeds, drop a 1024×1024 PNG intosrc-tauri/icons/source.pngand runpnpm tauri icon src-tauri/icons/source.pngto generate every required size.
Configuration
%APPDATA%\claude-widget\config.json (auto-created on first run):
{
"caps": {
"block_tokens": 200000, // 5h block cap — placeholder default
"weekly_tokens": 2000000 // 7d weekly cap — placeholder default
},
"wsl_distro_override": null, // null = autodetect via `wsl.exe -l -q`
"include_native": true, // also scan %USERPROFILE%\.claude\projects
"window_pos": null,
"autostart": false
}
Everything except window_pos is editable in the in-app Settings panel (gear icon).
Verification checklist
- Cold parse correctness — compare
BlockRingtotal to:jq -s '[.[]|select(.type=="assistant")|.message.usage|(.input_tokens+.output_tokens+.cache_creation_input_tokens+.cache_read_input_tokens)]|add' \ ~/.claude/projects/<path>/<sessionId>.jsonl - Block boundary —
scripts\seed-fake-jsonl.ps1 -OffsetHours -6appends a back-dated synthetic line; confirm it produces a new prior block instead of folding into the active one. - Dedupe — duplicate one assistant line into the matching
subagents/<id>.jsonl; total must not double. - Live tail — start a real
claudesession in WSL; the ring should tick up within a couple of seconds (debouncer ~250 ms). - Watcher fallback — set
WIDGET_NO_WATCH=1in the env (TODO: wire this up if needed) and append a line; the next 60s poll picks it up. - Fake feed —
scripts\seed-fake-jsonl.ps1writes a synthetic assistant line; UI updates without restart. - WSL detection — switch
wsl_distro_overridein Settings to a distro with no.claude/; snapshot goes empty. - Autostart — toggle on, reboot, confirm widget appears (Task Manager → Startup tab); toggle off, reboot, confirm it doesn't.
- Transparency / drag — no chrome; title-bar drag moves the window; position survives restart.
- Memory ceiling — 24h soak, expect 40–80 MB RSS.
Files of interest
| File | Purpose |
|---|---|
src-tauri/src/jsonl.rs |
Streaming parse + model normalization + dedupe key |
src-tauri/src/usage.rs |
5-hour blocks, weekly window, snapshot builder (incl. unit tests) |
src-tauri/src/watch.rs |
notify debouncer + 60s poll fallback + emit |
src-tauri/src/paths.rs |
WSL detection, \\wsl$\… UNC path resolution |
src-tauri/src/commands.rs |
Tauri #[command] IPC surface |
src-tauri/tauri.conf.json |
Frameless / transparent / always-on-top window config |
src/components/*.svelte |
UI |
License
Private project. Not published.