feat(claude-binary): implement robust version selector with enhanced binary detection

This commit provides a comprehensive solution to Claude binary detection issues
by implementing a user-friendly version selector UI and improving the binary
discovery logic. It addresses all concerns raised in multiple PRs and comments.

Changes:
- Add ClaudeVersionSelector component for selecting from multiple installations
- Update ClaudeBinaryDialog to use version selector instead of manual path input
- Fix unused variable warning in production builds (claude.rs:442)
- Improve select_best_installation to handle production build restrictions
- Add listClaudeInstallations API endpoint to fetch all available installations
- Make Claude version indicator clickable to navigate to Settings
- Move Claude installation selector to General tab in Settings (per user request)
- Enhance dialog UX with loading states and clear installation instructions
- Add Radix UI radio-group dependency for version selector

Fixes:
- Production build warning about unused claude_path variable
- Version detection failures in production builds due to process restrictions
- Poor UX when Claude binary is not found (now shows helpful dialog)
- Inability to easily switch between multiple Claude installations

This implementation takes inspiration from:
- PR #3: Version selector dropdown approach (preferred by users)
- PR #4: Binary detection improvements and path validation
- PR #39: Additional detection methods and error handling
- Commit 5a29f9a: Shared claude binary detection module architecture

Addresses feedback from:
- getAsterisk/claudia#4 (comment): User preference for dropdown selector
- Production build restrictions that prevent version detection
- Need for better error handling when Claude is not installed

The solution provides a seamless experience whether Claude is installed via:
- npm/yarn/bun global installation
- nvm-managed Node.js versions
- Homebrew on macOS
- System-wide installation
- Local user installation (~/.local/bin, etc.)

Refs: #3, #4, #39, 5a29f9a
This commit is contained in:
Mufeed VH
2025-06-25 02:49:24 +05:30
parent 97290e5665
commit c48a63f170
14 changed files with 556 additions and 77 deletions

View File

@@ -6,9 +6,10 @@ use log::{info, warn, debug, error};
use anyhow::Result;
use std::cmp::Ordering;
use tauri::Manager;
use serde::{Serialize, Deserialize};
/// Represents a Claude installation with metadata
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaudeInstallation {
/// Full path to the Claude binary
pub path: String,
@@ -68,6 +69,55 @@ pub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result<String, Strin
}
}
/// Discovers all available Claude installations and returns them for selection
/// This allows UI to show a version selector
pub fn discover_claude_installations() -> Vec<ClaudeInstallation> {
info!("Discovering all Claude installations...");
let installations = discover_all_installations();
// Sort by version (highest first), then by source preference
let mut sorted = installations;
sorted.sort_by(|a, b| {
match (&a.version, &b.version) {
(Some(v1), Some(v2)) => {
// Compare versions in descending order (newest first)
match compare_versions(v2, v1) {
Ordering::Equal => {
// If versions are equal, prefer by source
source_preference(a).cmp(&source_preference(b))
}
other => other
}
}
(Some(_), None) => Ordering::Less, // Version comes before no version
(None, Some(_)) => Ordering::Greater,
(None, None) => source_preference(a).cmp(&source_preference(b))
}
});
sorted
}
/// Returns a preference score for installation sources (lower is better)
fn source_preference(installation: &ClaudeInstallation) -> u8 {
match installation.source.as_str() {
"which" => 1,
"homebrew" => 2,
"system" => 3,
source if source.starts_with("nvm") => 4,
"local-bin" => 5,
"claude-local" => 6,
"npm-global" => 7,
"yarn" | "yarn-global" => 8,
"bun" => 9,
"node-modules" => 10,
"home-bin" => 11,
"PATH" => 12,
_ => 13,
}
}
/// Discovers all Claude installations on the system
fn discover_all_installations() -> Vec<ClaudeInstallation> {
let mut installations = Vec::new();
@@ -263,19 +313,25 @@ fn extract_version_from_output(stdout: &[u8]) -> Option<String> {
/// Select the best installation based on version
fn select_best_installation(installations: Vec<ClaudeInstallation>) -> Option<ClaudeInstallation> {
// In production builds, version information may not be retrievable because
// spawning external processes can be restricted. We therefore no longer
// discard installations that lack a detected version the mere presence
// of a readable binary on disk is enough to consider it valid. We still
// prefer binaries with version information when it is available so that
// in development builds we keep the previous behaviour of picking the
// most recent version.
installations.into_iter()
.filter(|i| {
// Prefer installations with known versions
i.version.is_some() || i.path == "claude"
})
.max_by(|a, b| {
// First compare by version presence
match (&a.version, &b.version) {
// If both have versions, compare them semantically.
(Some(v1), Some(v2)) => compare_versions(v1, v2),
// Prefer the entry that actually has version information.
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
// Neither have version info: prefer the one that is not just
// the bare "claude" lookup from PATH, because that may fail
// at runtime if PATH is sandbox-stripped.
(None, None) => {
// Both have no version, prefer non-PATH entries
if a.path == "claude" && b.path != "claude" {
Ordering::Less
} else if a.path != "claude" && b.path == "claude" {

View File

@@ -1807,6 +1807,18 @@ pub async fn set_claude_binary_path(db: State<'_, AgentDb>, path: String) -> Res
Ok(())
}
/// List all available Claude installations on the system
#[tauri::command]
pub async fn list_claude_installations() -> Result<Vec<crate::claude_binary::ClaudeInstallation>, String> {
let installations = crate::claude_binary::discover_claude_installations();
if installations.is_empty() {
return Err("No Claude Code installations found on the system".to_string());
}
Ok(installations)
}
/// Helper function to create a tokio Command with proper environment variables
/// This ensures commands like Claude can find Node.js and other dependencies
fn create_command_with_env(program: &str) -> Command {

View File

@@ -440,6 +440,10 @@ pub async fn get_claude_settings() -> Result<ClaudeSettings, String> {
pub async fn open_new_session(app: AppHandle, path: Option<String>) -> Result<String, String> {
log::info!("Opening new Claude Code session at path: {:?}", path);
#[cfg(not(debug_assertions))]
let _claude_path = find_claude_binary(&app)?;
#[cfg(debug_assertions)]
let claude_path = find_claude_binary(&app)?;
// In production, we can't use std::process::Command directly

View File

@@ -24,12 +24,12 @@ use commands::agents::{
init_database, list_agents, create_agent, update_agent, delete_agent,
get_agent, execute_agent, list_agent_runs, get_agent_run,
get_agent_run_with_real_time_metrics, list_agent_runs_with_metrics,
migrate_agent_runs_to_session_ids, list_running_sessions, kill_agent_session,
list_running_sessions, kill_agent_session,
get_session_status, cleanup_finished_processes, get_session_output,
get_live_session_output, stream_session_output, get_claude_binary_path,
set_claude_binary_path, export_agent, export_agent_to_file, import_agent,
import_agent_from_file, fetch_github_agents, fetch_github_agent_content,
import_agent_from_github, AgentDb
import_agent_from_github, list_claude_installations, AgentDb
};
use commands::sandbox::{
list_sandbox_profiles, create_sandbox_profile, update_sandbox_profile, delete_sandbox_profile,
@@ -139,19 +139,11 @@ fn main() {
update_agent,
delete_agent,
get_agent,
export_agent,
export_agent_to_file,
import_agent,
import_agent_from_file,
fetch_github_agents,
fetch_github_agent_content,
import_agent_from_github,
execute_agent,
list_agent_runs,
get_agent_run,
get_agent_run_with_real_time_metrics,
list_agent_runs_with_metrics,
migrate_agent_runs_to_session_ids,
get_agent_run_with_real_time_metrics,
list_running_sessions,
kill_agent_session,
get_session_status,
@@ -161,6 +153,14 @@ fn main() {
stream_session_output,
get_claude_binary_path,
set_claude_binary_path,
list_claude_installations,
export_agent,
export_agent_to_file,
import_agent,
import_agent_from_file,
fetch_github_agents,
fetch_github_agent_content,
import_agent_from_github,
list_sandbox_profiles,
get_sandbox_profile,
create_sandbox_profile,