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 - Commit5a29f9a
: 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:
@@ -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" {
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
|
Reference in New Issue
Block a user