claude-usage-widget/README.md

113 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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](https://v2.tauri.app/start/prerequisites/). Quick version:
```powershell
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:
```powershell
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 build` succeeds, drop a 1024×1024 PNG into `src-tauri/icons/source.png` and run `pnpm tauri icon src-tauri/icons/source.png` to generate every required size.
## Configuration
`%APPDATA%\claude-widget\config.json` (auto-created on first run):
```jsonc
{
"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
1. **Cold parse correctness** — compare `BlockRing` total to:
```bash
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
```
2. **Block boundary** — `scripts\seed-fake-jsonl.ps1 -OffsetHours -6` appends a back-dated synthetic line; confirm it produces a *new* prior block instead of folding into the active one.
3. **Dedupe** — duplicate one assistant line into the matching `subagents/<id>.jsonl`; total must not double.
4. **Live tail** — start a real `claude` session in WSL; the ring should tick up within a couple of seconds (debouncer ~250 ms).
5. **Watcher fallback** — set `WIDGET_NO_WATCH=1` in the env (TODO: wire this up if needed) and append a line; the next 60s poll picks it up.
6. **Fake feed** — `scripts\seed-fake-jsonl.ps1` writes a synthetic assistant line; UI updates without restart.
7. **WSL detection** — switch `wsl_distro_override` in Settings to a distro with no `.claude/`; snapshot goes empty.
8. **Autostart** — toggle on, reboot, confirm widget appears (Task Manager → Startup tab); toggle off, reboot, confirm it doesn't.
9. **Transparency / drag** — no chrome; title-bar drag moves the window; position survives restart.
10. **Memory ceiling** — 24h soak, expect 4080 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.