Auto-detect 'wsl.exe -d Ubuntu bash -lc claude' as default on Windows; record milestone in memory
This commit is contained in:
parent
97864374aa
commit
75049903e7
2 changed files with 50 additions and 18 deletions
26
memory.md
26
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 `<rect>`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$\<distro>\home\<user>\.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$\<distro>\home\<user>\.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
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,39 @@ pub fn fetch_blocking(command_override: Option<&str>) -> Result<CliUsage> {
|
|||
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<Vec<u8>> {
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue