Add seed-fake-jsonl.ps1 verification helper and README
This commit is contained in:
parent
0e8a87fbc5
commit
c1ef514697
2 changed files with 196 additions and 0 deletions
113
README.md
Normal file
113
README.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# 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 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.
|
||||
83
scripts/seed-fake-jsonl.ps1
Normal file
83
scripts/seed-fake-jsonl.ps1
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Verification helper: append a synthetic assistant line to a JSONL under one
|
||||
# of the resolved roots, so the watcher/poll can be exercised without running
|
||||
# a real Claude Code session.
|
||||
#
|
||||
# Usage (from PowerShell on the Windows host):
|
||||
# .\scripts\seed-fake-jsonl.ps1
|
||||
# .\scripts\seed-fake-jsonl.ps1 -InputTokens 5000 -OutputTokens 1500 -Model claude-sonnet-4-6
|
||||
# .\scripts\seed-fake-jsonl.ps1 -OffsetHours -6 # back-date by 6h to test block boundary
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string] $Distro = $null, # null → first distro found
|
||||
[string] $Model = "claude-opus-4-7",
|
||||
[int] $InputTokens = 100,
|
||||
[int] $OutputTokens = 500,
|
||||
[int] $CacheCreate = 0,
|
||||
[int] $CacheRead = 0,
|
||||
[double] $OffsetHours = 0, # back/forward date the timestamp
|
||||
[string] $Path = $null # explicit JSONL path; overrides discovery
|
||||
)
|
||||
|
||||
function Find-RootJsonl {
|
||||
param([string] $Distro)
|
||||
|
||||
if ($Path) { return $Path }
|
||||
|
||||
$candidates = @()
|
||||
|
||||
# Native first.
|
||||
$native = Join-Path $env:USERPROFILE ".claude\projects"
|
||||
if (Test-Path $native) { $candidates += $native }
|
||||
|
||||
# WSL.
|
||||
if (-not $Distro) {
|
||||
$distros = (& wsl.exe -l -q) | Where-Object { $_ -and $_.Trim() }
|
||||
if ($distros) { $Distro = $distros[0].Trim() }
|
||||
}
|
||||
if ($Distro) {
|
||||
$homeDir = "\\wsl$\$Distro\home"
|
||||
if (Test-Path $homeDir) {
|
||||
Get-ChildItem $homeDir -Directory | ForEach-Object {
|
||||
$p = Join-Path $_.FullName ".claude\projects"
|
||||
if (Test-Path $p) { $candidates += $p }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $candidates) { throw "No .claude/projects found (native or WSL)." }
|
||||
|
||||
$root = $candidates[0]
|
||||
Write-Host "Using root: $root"
|
||||
|
||||
# Use a dedicated synthetic project dir so we don't pollute real transcripts.
|
||||
$project = Join-Path $root "-fake-claude-usage-widget"
|
||||
New-Item -ItemType Directory -Force -Path $project | Out-Null
|
||||
return Join-Path $project "synthetic.jsonl"
|
||||
}
|
||||
|
||||
$target = Find-RootJsonl -Distro $Distro
|
||||
$ts = (Get-Date).ToUniversalTime().AddHours($OffsetHours).ToString("o")
|
||||
$uuid = [guid]::NewGuid().ToString()
|
||||
$reqId = "req_" + ([guid]::NewGuid().ToString("N").Substring(0, 16))
|
||||
|
||||
$line = @{
|
||||
type = "assistant"
|
||||
timestamp = $ts
|
||||
sessionId = $uuid
|
||||
requestId = $reqId
|
||||
uuid = $uuid
|
||||
message = @{
|
||||
model = $Model
|
||||
usage = @{
|
||||
input_tokens = $InputTokens
|
||||
output_tokens = $OutputTokens
|
||||
cache_creation_input_tokens = $CacheCreate
|
||||
cache_read_input_tokens = $CacheRead
|
||||
}
|
||||
}
|
||||
} | ConvertTo-Json -Compress -Depth 6
|
||||
|
||||
Add-Content -Path $target -Value $line -Encoding UTF8
|
||||
Write-Host "Appended to $target"
|
||||
Write-Host $line
|
||||
Loading…
Add table
Add a link
Reference in a new issue