From f33bb5481b0581f578a80453e81f6ae30f98c06b Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 9 May 2026 01:00:50 +0100 Subject: [PATCH] Detect tier across WSL roots, not just native home (\\wsl$\\... probe) --- src-tauri/src/commands.rs | 27 ++++++++++++-- src-tauri/src/settings.rs | 18 +++++++++- src-tauri/src/tier.rs | 75 +++++++++++++++++++++++---------------- 3 files changed, 86 insertions(+), 34 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index f8f2afc..270c6bf 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,9 +1,11 @@ //! Tauri command surface — every JS-callable function lives here. +use std::path::PathBuf; + use crate::paths::{list_wsl_distros, resolve_roots, ResolvedRoots}; use crate::settings::{save as save_settings, Caps, Settings}; use crate::state::SharedState; -use crate::tier::{detect as detect_tier, PlanTier}; +use crate::tier::{detect_in as detect_tier_in, PlanTier}; use crate::usage::{build_snapshot, UsageSnapshot}; use crate::watch::refresh_and_emit; @@ -84,8 +86,27 @@ pub struct TierInfo { } #[tauri::command] -pub async fn detect_plan_tier() -> Result { - let tier = detect_tier(); +pub async fn detect_plan_tier( + state: tauri::State<'_, SharedState>, +) -> Result { + // Each resolved root is a `/.claude/projects` path. The tier + // file lives in the parent (`/.claude/`) — so check those, plus + // the native home as a fallback, so this works whether Claude Code + // runs in WSL (UNC paths) or natively. + let mut candidates: Vec = state + .roots + .read() + .iter() + .filter_map(|r| r.parent().map(PathBuf::from)) + .collect(); + if let Some(home) = dirs::home_dir() { + candidates.push(home.join(".claude")); + } + // Dedupe (canonicalize is unreliable on UNC paths, so compare raw). + candidates.sort(); + candidates.dedup(); + + let tier = detect_tier_in(&candidates); let label = tier.label(); let recommended_caps = tier.caps(); Ok(TierInfo { tier, label, recommended_caps }) diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index c6917c5..7a7e0c2 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -65,8 +65,24 @@ pub fn load() -> Settings { let Ok(bytes) = std::fs::read(&path) else { // First run — seed caps from detected plan tier so the ring // doesn't peg red against the static placeholders. + // + // We don't have resolved roots yet at this point in startup, so + // also check WSL homes (without a watcher running) by re-resolving + // from default settings. + let resolved = crate::paths::resolve_roots(None, true); + let mut candidates: Vec = resolved + .roots + .iter() + .filter_map(|r| r.parent().map(std::path::PathBuf::from)) + .collect(); + if let Some(home) = dirs::home_dir() { + candidates.push(home.join(".claude")); + } + candidates.sort(); + candidates.dedup(); + let mut s = Settings::default(); - s.caps = crate::tier::detect().caps(); + s.caps = crate::tier::detect_in(&candidates).caps(); return s; }; match serde_json::from_slice::(&bytes) { diff --git a/src-tauri/src/tier.rs b/src-tauri/src/tier.rs index eb2e9df..c88288a 100644 --- a/src-tauri/src/tier.rs +++ b/src-tauri/src/tier.rs @@ -70,23 +70,18 @@ fn classify(raw: &str) -> PlanTier { } } -/// Find the most recent on-disk source of `organizationRateLimitTier`. -/// -/// Search order: -/// 1. `~/.claude/.claude.json` if it exists (Linux / WSL) -/// 2. `%USERPROFILE%\.claude\.claude.json` (Windows native — same logic via dirs::home_dir) -/// 3. The newest `~/.claude/backups/.claude.json.backup.*` (this is what -/// we observed populating in practice — `.claude.json` itself isn't -/// always present, but the backup directory tracks every config change) -fn locate_claude_json() -> Option { - let home = dirs::home_dir()?; - let primary = home.join(".claude").join(".claude.json"); - if primary.is_file() { - return Some(primary); - } +/// Read the tier from a single `.claude.json`-shaped file. None if the file +/// doesn't exist, isn't valid JSON, or is missing the field. +fn read_tier_from(path: &std::path::Path) -> Option { + let bytes = std::fs::read(path).ok()?; + let json: serde_json::Value = serde_json::from_slice(&bytes).ok()?; + let raw = json.get("organizationRateLimitTier").and_then(|v| v.as_str())?; + Some(classify(raw)) +} - let backups_dir = home.join(".claude").join("backups"); - let entries = std::fs::read_dir(&backups_dir).ok()?; +/// Find the newest `.claude.json.backup.*` in `dir` and read its tier. +fn read_tier_from_backups(dir: &std::path::Path) -> Option { + let entries = std::fs::read_dir(dir).ok()?; let mut latest: Option<(std::time::SystemTime, PathBuf)> = None; for entry in entries.flatten() { let path = entry.path(); @@ -104,23 +99,43 @@ fn locate_claude_json() -> Option { } } } - latest.map(|(_, p)| p) + let (_, path) = latest?; + read_tier_from(&path) } -pub fn detect() -> PlanTier { - let Some(path) = locate_claude_json() else { - return PlanTier::NotFound; - }; - let Ok(bytes) = std::fs::read(&path) else { - return PlanTier::NotFound; - }; - let Ok(json) = serde_json::from_slice::(&bytes) else { - return PlanTier::NotFound; - }; - match json.get("organizationRateLimitTier").and_then(|v| v.as_str()) { - Some(raw) => classify(raw), - None => PlanTier::NotFound, +/// Try each `.claude/` candidate directory in order. The first hit wins. +/// +/// For each candidate `` we look at: +/// 1. `/.claude.json` (e.g. ~/.claude/.claude.json) +/// 2. `/backups/.claude.json.backup.*` (newest) +/// 3. `/.claude.json` (some setups put it at ~/.claude.json) +pub fn detect_in(claude_dirs: &[PathBuf]) -> PlanTier { + for dir in claude_dirs { + let primary = dir.join(".claude.json"); + if let Some(t) = read_tier_from(&primary) { + return t; + } + let backups = dir.join("backups"); + if let Some(t) = read_tier_from_backups(&backups) { + return t; + } + if let Some(parent) = dir.parent() { + let alt = parent.join(".claude.json"); + if let Some(t) = read_tier_from(&alt) { + return t; + } + } } + PlanTier::NotFound +} + +/// Native-only fallback path. Most callers should prefer `detect_in` with +/// the resolved-roots' parents so the WSL case works. +pub fn detect() -> PlanTier { + let Some(home) = dirs::home_dir() else { + return PlanTier::NotFound; + }; + detect_in(&[home.join(".claude")]) } #[cfg(test)]