From 75049903e736863db89060f589ee33c43c9a71b0 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 9 May 2026 01:55:51 +0100 Subject: [PATCH] Auto-detect 'wsl.exe -d Ubuntu bash -lc claude' as default on Windows; record milestone in memory --- memory.md | 26 ++++++++++++----------- src-tauri/src/cli_usage.rs | 42 ++++++++++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/memory.md b/memory.md index bdb8b25..0b5b109 100644 --- a/memory.md +++ b/memory.md @@ -5,24 +5,26 @@ Durable memory for this project. Read at session start, update before session en ## Decisions & rationale - **Tauri 2 (Rust + Svelte 5 + Vite + TS)** over Electron — smaller binary (~10 MB vs ~150 MB), native Windows transparency, real always-on-top z-order. Chose Svelte over React because the widget has only three SVG primitives; React boilerplate isn't worth it. -- **Inline SVG, no chart library** — `BlockRing` is one circle, `WeeklyBar` is seven ``s, `ModelStack` is a stacked single-row bar. Adding Chart.js / ECharts / uPlot for ~80 lines of SVG would balloon the bundle for nothing. -- **JSONL-only data source, no Anthropic API** — Anthropic doesn't expose a local cap-state file, but the JSONL transcripts contain everything we need to derive usage (this is what `ccusage` does). Avoids needing an admin key and keeps the widget fully offline. -- **Widget runs on Windows host, not in WSLg** — needs to pin to the Windows desktop, autostart on login, and share the always-on-top z-order with native Windows apps. WSLg windows can't do that. The widget reads WSL transcripts via the `\\wsl$\\home\\.claude\projects\` UNC mount. -- **`notify` watcher + 60s tokio poll fallback** — `ReadDirectoryChangesW` on the WSL 9P mount is unreliable; the poll backstops it. 60 s is a pragmatic balance vs CPU. +- **Inline SVG, no chart library** — `BlockRing` is one ring, `WeeklyBar` is two stacked progress bars, `ModelStack` is a single segmented bar. Adding Chart.js / ECharts / uPlot for ~80 lines of SVG would balloon the bundle for nothing. +- **Subscription %s come from PTY-driving `claude /usage`** (NOT JSONL estimates, NOT the Anthropic API). The widget spawns `claude` via `portable-pty`, sends `/usage`, parses the three rendered bars (Current session / Current week all / Current week Sonnet), and shows those exact numbers. This is the same data Anthropic shows you in the CLI — no API key, no admin scope, no reverse-engineering of their backend. The trade-off is ~3-5 s per refresh and brittleness if Anthropic changes the rendered output format. Refresh every 5 min by default; manual refresh button on the title bar. +- **Per-model breakdown still comes from local JSONL** — the CLI's `/usage` doesn't break out Opus/Sonnet/Haiku, but our token-summing does. ModelStack remains. +- **Widget runs on Windows host, not in WSLg** — needs to pin to the Windows desktop, autostart on login, and share the always-on-top z-order with native Windows apps. WSLg windows can't do that. JSONL transcripts read via `\\wsl$\\home\\.claude\projects\` UNC mount; the PTY-driven `claude` is invoked via `wsl.exe -d Ubuntu bash -lc claude` (default on Windows when wsl.exe is on PATH). +- **`notify` watcher + 60s tokio poll fallback** — `ReadDirectoryChangesW` on the WSL 9P mount is unreliable; the poll backstops it. - **All filesystem reads happen Rust-side** — the JS `capabilities/default.json` does NOT grant `tauri-plugin-fs`. Keeps the webview sandbox tight. -- **Block algorithm** — `block_start = floor_to_hour(first_ts_of_block)`, `block_end = block_start + 5h`, new block on ≥5h gap OR when previous block ends. This matches ccusage and the way Anthropic's docs describe the rolling window. -- **Weekly = rolling 7 days, no calendar anchoring** — Anthropic's reported Max-plan weekly reset day is buggy and shifts (see GH issues #54974, #52921). The honest thing is "past 7 days from now." -- **Caps are user-configurable in Settings** with placeholder defaults (200k tokens / 5h block, 2M tokens / week). No authoritative local source for the real caps. +- **Block algorithm (still in code, used for ModelStack only)** — `block_start = floor_to_hour(first_ts_of_block)`, `block_end = block_start + 5h`, new block on ≥5h gap OR when previous block ends. ccusage-equivalent. +- **DROPPED: caps + tier-detection UI.** Replaced by real CLI percentages. Caps struct still exists in code as a deprecated fallback but the Settings panel no longer exposes it. ## Open questions / TODOs -- [ ] **Watcher does not re-bind on settings change.** If user changes WSL distro override in Settings, `set_settings` calls `refresh_and_emit` and updates `state.roots`, but the `notify` watcher is still pinned to the *old* roots. v0 workaround: restart the widget after changing distro. Better fix: rebuild `WatcherHandle` on settings change. -- [ ] Tune cap defaults once we have a few weeks of real data — current 200k / 2M values are guesses. +- [ ] **Watcher does not re-bind on settings change.** If user changes WSL distro override in Settings, `set_settings` calls `refresh_and_emit` and updates `state.roots`, but the `notify` watcher is still pinned to the *old* roots. v0 workaround: restart the widget after changing distro. +- [ ] **`/usage` parser is fragile to output format changes.** If Anthropic changes the rendered text (relabels sections, adds new ones, changes "X% used" pattern), the bars stop parsing silently. Settings panel exposes the raw output for debugging when this happens. +- [ ] **`/usage` spawn cost is ~3-5s on Windows.** That's per refresh; default refresh is 300s so net overhead is fine. Title-bar refresh button gives user control. Consider caching to disk so cold start has *something* before the first PTY drive completes. +- [ ] **Autostart toggle fails in dev builds** (target\debug\ exe path is unstable). Currently swallowed as a warning; needs proper testing once we ship the NSIS bundle. +- [ ] **The default WSL-on-Windows command assumes Ubuntu.** Auto-detect could iterate `wsl.exe -l -q` for any distro that has `claude` on its login PATH, instead of hardcoding `-d Ubuntu`. - [ ] Decide whether to expose a tray icon for relaunch after `quit_app` (currently the widget can only be reopened via Start Menu / autostart). -- [ ] Consider whether to fold in the pricing / `$ estimate` view later — out of scope for v0 per user. -- [ ] Verify subagent dedupe assumption: do subagent JSONLs ever contain assistant lines that aren't also in the parent transcript? If yes, we MUST count them; if always duplicate, we MUST skip them. Code uses `requestId || uuid` set, which is safe either way. +- [ ] Window background is too transparent — files behind it bleed through visibly. Bump `--bg` opacity from 0.78 to ~0.92. - [ ] Replace placeholder Tauri icons in `src-tauri/icons/` before release (`pnpm tauri icon source.png`). -- [ ] First `cargo check` / `pnpm install` has not run — toolchain absent in WSL. Build will happen on Windows host; expect minor compile warnings on first try. +- [ ] Caps struct + Caps::default() are dead code now — delete after a few releases of stability. ## Session log diff --git a/src-tauri/src/cli_usage.rs b/src-tauri/src/cli_usage.rs index e384dc4..7a86986 100644 --- a/src-tauri/src/cli_usage.rs +++ b/src-tauri/src/cli_usage.rs @@ -65,6 +65,39 @@ pub fn fetch_blocking(command_override: Option<&str>) -> Result { parse_usage_text(&stripped, raw) } +/// Pick a sensible default command line for invoking `claude`. +/// +/// On Windows, `claude` may resolve to a Windows-native install that isn't +/// authenticated, while the user's real session lives in WSL. Prefer the +/// WSL Ubuntu invocation when a `wsl.exe` is detectable on PATH. +/// +/// On Linux/macOS, just `claude`. +fn default_command() -> CommandBuilder { + if cfg!(windows) { + // Probe for wsl.exe; if present, run claude through a login bash in + // the Ubuntu distro (the most common dev setup, and the user's PATH + // is wired through .profile / .bashrc so `claude` resolves). + if which_exists("wsl.exe") { + let mut c = CommandBuilder::new("wsl.exe"); + for a in ["-d", "Ubuntu", "bash", "-lc", "claude"] { + c.arg(a); + } + return c; + } + } + CommandBuilder::new("claude") +} + +fn which_exists(name: &str) -> bool { + use std::process::Command; + let probe = if cfg!(windows) { "where" } else { "which" }; + Command::new(probe) + .arg(name) + .output() + .map(|o| o.status.success() && !o.stdout.is_empty()) + .unwrap_or(false) +} + /// Spawn the CLI in a PTY, send `/usage`, capture stdout for `total_timeout`, /// then send `/exit` and return raw bytes (still containing ANSI escapes). fn drive_claude_usage(command_override: Option<&str>, total_timeout: Duration) -> Result> { @@ -79,19 +112,16 @@ fn drive_claude_usage(command_override: Option<&str>, total_timeout: Duration) - .context("openpty")?; let mut cmd = match command_override { - Some(s) => { - // Allow simple "wsl.exe -- claude" style strings. + Some(s) if !s.trim().is_empty() => { + // Allow simple "wsl.exe -d Ubuntu bash -lc claude" style strings. let parts: Vec<&str> = s.split_whitespace().collect(); - if parts.is_empty() { - return Err(anyhow!("empty command_override")); - } let mut c = CommandBuilder::new(parts[0]); for arg in &parts[1..] { c.arg(arg); } c } - None => CommandBuilder::new("claude"), + _ => default_command(), }; // claude inspects $TERM; give it something reasonable. cmd.env("TERM", "xterm-256color");