From 18e55cd139beea26236b1fa5fa4b73e267056f14 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 9 May 2026 01:19:52 +0100 Subject: [PATCH] =?UTF-8?q?tier:=20per-path=20probe=20diagnostics=20?= =?UTF-8?q?=E2=80=94=20surface=20read=20errors=20/=20parse=20status=20in?= =?UTF-8?q?=20TierInfo.searched?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands.rs | 10 ++-- src-tauri/src/tier.rs | 108 +++++++++++++++++++++++++++----------- 2 files changed, 80 insertions(+), 38 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 6efe312..3b7da4c 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -5,7 +5,7 @@ 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_in as detect_tier_in, PlanTier}; +use crate::tier::{detect_in_with_diagnostics, PlanTier}; use crate::usage::{build_snapshot, UsageSnapshot}; use crate::watch::refresh_and_emit; @@ -111,12 +111,8 @@ pub async fn detect_plan_tier( candidates.sort(); candidates.dedup(); - let tier = detect_tier_in(&candidates); + let (tier, diagnostics) = detect_in_with_diagnostics(&candidates); let label = tier.label(); let recommended_caps = tier.caps(); - Ok(TierInfo { tier, label, recommended_caps, searched: paths_to_strings(&candidates) }) -} - -fn paths_to_strings(paths: &[PathBuf]) -> Vec { - paths.iter().map(|p| p.display().to_string()).collect() + Ok(TierInfo { tier, label, recommended_caps, searched: diagnostics }) } diff --git a/src-tauri/src/tier.rs b/src-tauri/src/tier.rs index fe7c8c2..8bf9170 100644 --- a/src-tauri/src/tier.rs +++ b/src-tauri/src/tier.rs @@ -79,9 +79,82 @@ fn read_tier_from(path: &std::path::Path) -> Option { Some(classify(raw)) } -/// 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()?; +/// Same as [`read_tier_from`] but returns a string describing what happened. +/// Used for the diagnostic `searched` list in TierInfo. +fn probe_tier_file(path: &std::path::Path) -> String { + match std::fs::read(path) { + Err(e) => format!("read_err: {}", e.kind()), + Ok(bytes) => match serde_json::from_slice::(&bytes) { + Err(_) => format!("not_json (size={})", bytes.len()), + Ok(json) => match json.get("organizationRateLimitTier").and_then(|v| v.as_str()) { + None => format!("no_tier_field (size={})", bytes.len()), + Some(raw) => format!("OK: {}", raw), + }, + }, + } +} + +/// 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 { + let (tier, _) = detect_in_with_diagnostics(claude_dirs); + tier +} + +/// Like [`detect_in`] but also returns one diagnostic line per probed path +/// describing what happened (for surfacing in the Settings UI). +pub fn detect_in_with_diagnostics( + claude_dirs: &[PathBuf], +) -> (PlanTier, Vec) { + let mut diag: Vec = Vec::new(); + let mut found: Option = None; + + for dir in claude_dirs { + let primary = dir.join(".claude.json"); + let result = probe_tier_file(&primary); + diag.push(format!("{} → {}", primary.display(), result)); + if found.is_none() { + if let Some(t) = read_tier_from(&primary) { + found = Some(t); + } + } + + let backups = dir.join("backups"); + match newest_backup(&backups) { + Err(e) => diag.push(format!("{} → readdir_err: {}", backups.display(), e)), + Ok(None) => diag.push(format!("{} → no_backups", backups.display())), + Ok(Some(p)) => { + let result = probe_tier_file(&p); + diag.push(format!("{} → {}", p.display(), result)); + if found.is_none() { + if let Some(t) = read_tier_from(&p) { + found = Some(t); + } + } + } + } + + if let Some(parent) = dir.parent() { + let alt = parent.join(".claude.json"); + let result = probe_tier_file(&alt); + diag.push(format!("{} → {}", alt.display(), result)); + if found.is_none() { + if let Some(t) = read_tier_from(&alt) { + found = Some(t); + } + } + } + } + + (found.unwrap_or(PlanTier::NotFound), diag) +} + +fn newest_backup(dir: &std::path::Path) -> std::io::Result> { + let entries = std::fs::read_dir(dir)?; let mut latest: Option<(std::time::SystemTime, PathBuf)> = None; for entry in entries.flatten() { let path = entry.path(); @@ -99,34 +172,7 @@ fn read_tier_from_backups(dir: &std::path::Path) -> Option { } } } - let (_, path) = latest?; - read_tier_from(&path) -} - -/// 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 + Ok(latest.map(|(_, p)| p)) } /// Native-only fallback path. Most callers should prefer `detect_in` with