Polish for shipping: robust auto-detect, empty state, real icons, end-user README

- cli_usage::default_command now enumerates WSL distros and probes each for
  claude before falling back; no more hardcoded -d Ubuntu.
- New autodetect_claude_command Tauri command + IPC binding so the UI knows
  whether claude is reachable.
- App.svelte: clear 'Claude Code not found' empty state with install link.
- Real icons: scripts/make-icon.py generates a 1024x1024 source.png; runtime
  produces 32/128/256 PNGs and a multi-resolution .ico. README in icons/
  explains how to regen.
- README rewritten for friends: install / requirements / troubleshooting on
  top; build-from-source moved to bottom.
This commit is contained in:
megaproxy 2026-05-09 14:25:24 +01:00
parent 0a960dae2d
commit 9be856d37c
14 changed files with 321 additions and 128 deletions

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -1,13 +1,19 @@
# Icons
Placeholder. Before `pnpm tauri build` will succeed you need real icons here.
`source.png` — 1024×1024 master icon. Dark rounded square + purple progress
ring + white "C". Generated by `../../scripts/make-icon.py`.
Quickest path: `pnpm tauri icon path/to/source-1024x1024.png` — Tauri generates every required size + format (`.ico`, `.icns`, `.png`).
To regenerate every required size + format Tauri's bundler needs:
Files Tauri's bundler expects (referenced from `tauri.conf.json`):
```sh
pnpm tauri icon src-tauri/icons/source.png
```
- `32x32.png`
- `128x128.png`
- `128x128@2x.png`
- `icon.icns`
- `icon.ico`
That populates `32x32.png`, `128x128.png`, `128x128@2x.png`, `icon.icns`,
`icon.ico`, plus Android/iOS sizes (we ignore those — desktop only).
The generated icons are tracked in git so a clean clone can `pnpm tauri build`
without first running `tauri icon`.
To customize: edit `scripts/make-icon.py` (colors, progress sweep, monogram)
and rerun.

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

BIN
src-tauri/icons/source.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -67,27 +67,52 @@ pub fn fetch_blocking(command_override: Option<&str>) -> Result<CliUsage> {
/// 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.
/// Order:
/// 1. Native `claude` (Windows: `claude.exe` on PATH; Unix: `claude`).
/// 2. On Windows: enumerate WSL distros via `wsl.exe -l -q` and probe
/// each by running `bash -lc 'command -v claude'`. First hit wins.
/// 3. Fallback: bare `claude` (will fail, but at least with a clear error).
///
/// On Linux/macOS, just `claude`.
/// This is called fresh on every `/usage` fetch, but each probe is cheap
/// (<200ms typical) and only runs when no override is set.
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;
if let Some(parts) = autodetect_command() {
let mut c = CommandBuilder::new(&parts[0]);
for a in &parts[1..] {
c.arg(a);
}
return c;
}
CommandBuilder::new("claude")
}
/// Returns the auto-detected argv (program + args) for invoking claude, or
/// None if nothing reachable was found.
pub fn autodetect_command() -> Option<Vec<String>> {
// 1. Native claude.
if which_exists("claude") {
return Some(vec!["claude".to_string()]);
}
// 2. WSL distros (Windows only).
if cfg!(windows) && which_exists("wsl.exe") {
for distro in list_wsl_distros() {
if probe_claude_in_wsl(&distro) {
return Some(vec![
"wsl.exe".to_string(),
"-d".to_string(),
distro,
"bash".to_string(),
"-lc".to_string(),
"claude".to_string(),
]);
}
}
}
None
}
fn which_exists(name: &str) -> bool {
use std::process::Command;
let probe = if cfg!(windows) { "where" } else { "which" };
@ -98,6 +123,36 @@ fn which_exists(name: &str) -> bool {
.unwrap_or(false)
}
fn list_wsl_distros() -> Vec<String> {
use std::process::Command;
let Ok(out) = Command::new("wsl.exe").args(["-l", "-q"]).output() else {
return Vec::new();
};
if !out.status.success() {
return Vec::new();
}
// wsl.exe outputs UTF-16LE.
let raw_u16: Vec<u16> = out
.stdout
.chunks_exact(2)
.map(|b| u16::from_le_bytes([b[0], b[1]]))
.collect();
String::from_utf16_lossy(&raw_u16)
.lines()
.map(|l| l.trim_matches(|c: char| c == '\u{FEFF}' || c.is_whitespace()).to_string())
.filter(|l| !l.is_empty())
.collect()
}
fn probe_claude_in_wsl(distro: &str) -> bool {
use std::process::Command;
Command::new("wsl.exe")
.args(["-d", distro, "bash", "-lc", "command -v claude"])
.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>> {

View file

@ -83,6 +83,13 @@ pub async fn get_cli_usage(
Ok(state.cli_usage.read().clone())
}
/// What the auto-detect found. Used by the empty-state UI to tell the
/// user whether claude is even reachable.
#[tauri::command]
pub async fn autodetect_claude_command() -> Result<Option<Vec<String>>, String> {
Ok(crate::cli_usage::autodetect_command())
}
/// Force-refresh /usage by spawning the CLI now. Slow (~3-5s); use sparingly.
#[tauri::command]
pub async fn refresh_cli_usage(

View file

@ -106,6 +106,7 @@ pub fn run() {
commands::detect_plan_tier,
commands::get_cli_usage,
commands::refresh_cli_usage,
commands::autodetect_claude_command,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");