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

@@ -1,9 +1,9 @@
import { useState } from "react";
import { api } from "@/lib/api";
import { useState, useEffect } from "react";
import { api, type ClaudeInstallation } from "@/lib/api";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { ExternalLink, FileQuestion, Terminal } from "lucide-react";
import { ExternalLink, FileQuestion, Terminal, AlertCircle, Loader2 } from "lucide-react";
import { ClaudeVersionSelector } from "./ClaudeVersionSelector";
interface ClaudeBinaryDialogProps {
open: boolean;
@@ -13,18 +13,39 @@ interface ClaudeBinaryDialogProps {
}
export function ClaudeBinaryDialog({ open, onOpenChange, onSuccess, onError }: ClaudeBinaryDialogProps) {
const [binaryPath, setBinaryPath] = useState("");
const [selectedInstallation, setSelectedInstallation] = useState<ClaudeInstallation | null>(null);
const [isValidating, setIsValidating] = useState(false);
const [hasInstallations, setHasInstallations] = useState(true);
const [checkingInstallations, setCheckingInstallations] = useState(true);
useEffect(() => {
if (open) {
checkInstallations();
}
}, [open]);
const checkInstallations = async () => {
try {
setCheckingInstallations(true);
const installations = await api.listClaudeInstallations();
setHasInstallations(installations.length > 0);
} catch (error) {
// If the API call fails, it means no installations found
setHasInstallations(false);
} finally {
setCheckingInstallations(false);
}
};
const handleSave = async () => {
if (!binaryPath.trim()) {
onError("Please enter a valid path");
if (!selectedInstallation) {
onError("Please select a Claude installation");
return;
}
setIsValidating(true);
try {
await api.setClaudeBinaryPath(binaryPath.trim());
await api.setClaudeBinaryPath(selectedInstallation.path);
onSuccess();
onOpenChange(false);
} catch (error) {
@@ -37,46 +58,58 @@ export function ClaudeBinaryDialog({ open, onOpenChange, onSuccess, onError }: C
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FileQuestion className="w-5 h-5" />
Couldn't locate Claude Code installation
Select Claude Code Installation
</DialogTitle>
<DialogDescription className="space-y-3 mt-4">
<p>
Claude Code was not found in any of the common installation locations.
Please specify the path to the Claude binary manually.
</p>
<div className="flex items-center gap-2 p-3 bg-muted rounded-md">
<Terminal className="w-4 h-4 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
<span className="font-medium">Tip:</span> Run{" "}
<code className="px-1 py-0.5 bg-black/10 dark:bg-white/10 rounded">which claude</code>{" "}
in your terminal to find the installation path
{checkingInstallations ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-sm text-muted-foreground">Searching for Claude installations...</span>
</div>
) : hasInstallations ? (
<p>
Multiple Claude Code installations were found on your system.
Please select which one you'd like to use.
</p>
</div>
) : (
<>
<p>
Claude Code was not found in any of the common installation locations.
Please install Claude Code to continue.
</p>
<div className="flex items-center gap-2 p-3 bg-muted rounded-md">
<AlertCircle className="w-4 h-4 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
<span className="font-medium">Searched locations:</span> PATH, /usr/local/bin,
/opt/homebrew/bin, ~/.nvm/versions/node/*/bin, ~/.claude/local, ~/.local/bin
</p>
</div>
</>
)}
{!checkingInstallations && (
<div className="flex items-center gap-2 p-3 bg-muted rounded-md">
<Terminal className="w-4 h-4 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
<span className="font-medium">Tip:</span> You can install Claude Code using{" "}
<code className="px-1 py-0.5 bg-black/10 dark:bg-white/10 rounded">npm install -g @claude</code>
</p>
</div>
)}
</DialogDescription>
</DialogHeader>
<div className="py-4">
<Input
type="text"
placeholder="/usr/local/bin/claude"
value={binaryPath}
onChange={(e) => setBinaryPath(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !isValidating) {
handleSave();
}
}}
autoFocus
className="font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-2">
Common locations: /usr/local/bin/claude, /opt/homebrew/bin/claude, ~/.claude/local/claude
</p>
</div>
{!checkingInstallations && hasInstallations && (
<div className="py-4">
<ClaudeVersionSelector
onSelect={(installation) => setSelectedInstallation(installation)}
selectedPath={null}
/>
</div>
)}
<DialogFooter className="gap-3">
<Button
@@ -94,8 +127,11 @@ export function ClaudeBinaryDialog({ open, onOpenChange, onSuccess, onError }: C
>
Cancel
</Button>
<Button onClick={handleSave} disabled={isValidating || !binaryPath.trim()}>
{isValidating ? "Validating..." : "Save Path"}
<Button
onClick={handleSave}
disabled={isValidating || !selectedInstallation || !hasInstallations}
>
{isValidating ? "Validating..." : hasInstallations ? "Save Selection" : "No Installations Found"}
</Button>
</DialogFooter>
</DialogContent>