diff --git a/bun.lock b/bun.lock index 039b5ae..c6b28bf 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-popover": "^1.1.4", + "@radix-ui/react-radio-group": "^1.3.7", "@radix-ui/react-select": "^2.1.3", "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", @@ -286,6 +287,8 @@ "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g=="], + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q=="], "@radix-ui/react-select": ["@radix-ui/react-select@2.2.5", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA=="], diff --git a/package.json b/package.json index c6c9d66..f7db9e5 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-popover": "^1.1.4", + "@radix-ui/react-radio-group": "^1.3.7", "@radix-ui/react-select": "^2.1.3", "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", diff --git a/src-tauri/src/claude_binary.rs b/src-tauri/src/claude_binary.rs index 4751e1f..af46ef3 100644 --- a/src-tauri/src/claude_binary.rs +++ b/src-tauri/src/claude_binary.rs @@ -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 Vec { + 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 { let mut installations = Vec::new(); @@ -263,19 +313,25 @@ fn extract_version_from_output(stdout: &[u8]) -> Option { /// Select the best installation based on version fn select_best_installation(installations: Vec) -> Option { + // 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" { diff --git a/src-tauri/src/commands/agents.rs b/src-tauri/src/commands/agents.rs index ae1c634..a0cd9ed 100644 --- a/src-tauri/src/commands/agents.rs +++ b/src-tauri/src/commands/agents.rs @@ -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, 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 { diff --git a/src-tauri/src/commands/claude.rs b/src-tauri/src/commands/claude.rs index f5f908e..453f011 100644 --- a/src-tauri/src/commands/claude.rs +++ b/src-tauri/src/commands/claude.rs @@ -440,6 +440,10 @@ pub async fn get_claude_settings() -> Result { pub async fn open_new_session(app: AppHandle, path: Option) -> Result { 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 diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 2c55fe6..a5dad57 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -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, diff --git a/src/components/AgentExecution.tsx b/src/components/AgentExecution.tsx index fa325b6..de05488 100644 --- a/src/components/AgentExecution.tsx +++ b/src/components/AgentExecution.tsx @@ -70,7 +70,7 @@ export const AgentExecution: React.FC = ({ className, }) => { const [projectPath, setProjectPath] = useState(""); - const [task, setTask] = useState(""); + const [task, setTask] = useState(agent.default_task || ""); const [model, setModel] = useState(agent.model || "sonnet"); const [isRunning, setIsRunning] = useState(false); const [messages, setMessages] = useState([]); @@ -646,7 +646,7 @@ export const AgentExecution: React.FC = ({ setTask(e.target.value)} - placeholder={agent.default_task || "Enter the task for the agent"} + placeholder="Enter the task for the agent" disabled={isRunning} className="flex-1" onKeyPress={(e) => { diff --git a/src/components/ClaudeBinaryDialog.tsx b/src/components/ClaudeBinaryDialog.tsx index f5267d1..eb72ccd 100644 --- a/src/components/ClaudeBinaryDialog.tsx +++ b/src/components/ClaudeBinaryDialog.tsx @@ -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(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 ( - + - Couldn't locate Claude Code installation + Select Claude Code Installation -

- Claude Code was not found in any of the common installation locations. - Please specify the path to the Claude binary manually. -

-
- -

- Tip: Run{" "} - which claude{" "} - in your terminal to find the installation path + {checkingInstallations ? ( +

+ + Searching for Claude installations... +
+ ) : hasInstallations ? ( +

+ Multiple Claude Code installations were found on your system. + Please select which one you'd like to use.

-
+ ) : ( + <> +

+ Claude Code was not found in any of the common installation locations. + Please install Claude Code to continue. +

+
+ +

+ Searched locations: PATH, /usr/local/bin, + /opt/homebrew/bin, ~/.nvm/versions/node/*/bin, ~/.claude/local, ~/.local/bin +

+
+ + )} + {!checkingInstallations && ( +
+ +

+ Tip: You can install Claude Code using{" "} + npm install -g @claude +

+
+ )}
-
- setBinaryPath(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" && !isValidating) { - handleSave(); - } - }} - autoFocus - className="font-mono text-sm" - /> -

- Common locations: /usr/local/bin/claude, /opt/homebrew/bin/claude, ~/.claude/local/claude -

-
+ {!checkingInstallations && hasInstallations && ( +
+ setSelectedInstallation(installation)} + selectedPath={null} + /> +
+ )} -
diff --git a/src/components/ClaudeVersionSelector.tsx b/src/components/ClaudeVersionSelector.tsx new file mode 100644 index 0000000..1cc17d1 --- /dev/null +++ b/src/components/ClaudeVersionSelector.tsx @@ -0,0 +1,231 @@ +import React, { useEffect, useState } from "react"; +import { api, type ClaudeInstallation } from "@/lib/api"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Loader2, Terminal, Package, Check } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface ClaudeVersionSelectorProps { + /** + * Currently selected Claude installation path + */ + selectedPath?: string | null; + /** + * Callback when a Claude installation is selected + */ + onSelect: (installation: ClaudeInstallation) => void; + /** + * Optional className for styling + */ + className?: string; + /** + * Whether to show a save button (for settings page) + */ + showSaveButton?: boolean; + /** + * Callback when save button is clicked + */ + onSave?: () => void; + /** + * Whether the save operation is in progress + */ + isSaving?: boolean; +} + +/** + * ClaudeVersionSelector component for selecting Claude Code installations + * + * @example + * setSelectedInstallation(installation)} + * /> + */ +export const ClaudeVersionSelector: React.FC = ({ + selectedPath, + onSelect, + className, + showSaveButton = false, + onSave, + isSaving = false, +}) => { + const [installations, setInstallations] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedInstallation, setSelectedInstallation] = useState(null); + + useEffect(() => { + loadInstallations(); + }, []); + + useEffect(() => { + // Update selected installation when selectedPath changes + if (selectedPath && installations.length > 0) { + const found = installations.find(i => i.path === selectedPath); + if (found) { + setSelectedInstallation(found); + } + } + }, [selectedPath, installations]); + + const loadInstallations = async () => { + try { + setLoading(true); + setError(null); + const foundInstallations = await api.listClaudeInstallations(); + setInstallations(foundInstallations); + + // If we have a selected path, find and select it + if (selectedPath) { + const found = foundInstallations.find(i => i.path === selectedPath); + if (found) { + setSelectedInstallation(found); + } + } else if (foundInstallations.length > 0) { + // Auto-select the first (best) installation + setSelectedInstallation(foundInstallations[0]); + onSelect(foundInstallations[0]); + } + } catch (err) { + console.error("Failed to load Claude installations:", err); + setError(err instanceof Error ? err.message : "Failed to load Claude installations"); + } finally { + setLoading(false); + } + }; + + const handleSelect = (installation: ClaudeInstallation) => { + setSelectedInstallation(installation); + onSelect(installation); + }; + + const getSourceIcon = (source: string) => { + if (source.includes("nvm")) return ; + return ; + }; + + const getSourceLabel = (source: string) => { + if (source === "which") return "System PATH"; + if (source === "homebrew") return "Homebrew"; + if (source === "system") return "System"; + if (source.startsWith("nvm")) return source.replace("nvm ", "NVM "); + if (source === "local-bin") return "Local bin"; + if (source === "claude-local") return "Claude local"; + if (source === "npm-global") return "NPM global"; + if (source === "yarn" || source === "yarn-global") return "Yarn"; + if (source === "bun") return "Bun"; + return source; + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( + +
{error}
+
+ ); + } + + if (installations.length === 0) { + return ( + +
+ No Claude Code installations found on your system. +
+
+ ); + } + + return ( +
+
+ + { + const installation = installations.find(i => i.path === value); + if (installation) { + handleSelect(installation); + } + }} + > +
+ {installations.map((installation) => ( + handleSelect(installation)} + > +
+ +
+
+ {getSourceIcon(installation.source)} + + {getSourceLabel(installation.source)} + + {installation.version && ( + + v{installation.version} + + )} + {selectedPath === installation.path && ( + + + Current + + )} +
+

+ {installation.path} +

+
+
+
+ ))} +
+
+
+ + {showSaveButton && onSave && ( +
+ +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index d12e63e..b130243 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -20,10 +20,12 @@ import { Card } from "@/components/ui/card"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { api, - type ClaudeSettings + type ClaudeSettings, + type ClaudeInstallation } from "@/lib/api"; import { cn } from "@/lib/utils"; import { Toast, ToastContainer } from "@/components/ui/toast"; +import { ClaudeVersionSelector } from "./ClaudeVersionSelector"; interface SettingsProps { /** @@ -69,12 +71,30 @@ export const Settings: React.FC = ({ // Environment variables state const [envVars, setEnvVars] = useState([]); + // Claude binary path state + const [currentBinaryPath, setCurrentBinaryPath] = useState(null); + const [selectedInstallation, setSelectedInstallation] = useState(null); + const [binaryPathChanged, setBinaryPathChanged] = useState(false); + // Load settings on mount useEffect(() => { loadSettings(); + loadClaudeBinaryPath(); }, []); + /** + * Loads the current Claude binary path + */ + const loadClaudeBinaryPath = async () => { + try { + const path = await api.getClaudeBinaryPath(); + setCurrentBinaryPath(path); + } catch (err) { + console.error("Failed to load Claude binary path:", err); + } + }; + /** * Loads the current Claude settings */ @@ -159,6 +179,14 @@ export const Settings: React.FC = ({ await api.saveClaudeSettings(updatedSettings); setSettings(updatedSettings); + + // Save Claude binary path if changed + if (binaryPathChanged && selectedInstallation) { + await api.setClaudeBinaryPath(selectedInstallation.path); + setCurrentBinaryPath(selectedInstallation.path); + setBinaryPathChanged(false); + } + setToast({ message: "Settings saved successfully!", type: "success" }); } catch (err) { console.error("Failed to save settings:", err); @@ -246,6 +274,13 @@ export const Settings: React.FC = ({ setEnvVars(prev => prev.filter(envVar => envVar.id !== id)); }; + /** + * Handle Claude installation selection + */ + const handleClaudeInstallationSelect = (installation: ClaudeInstallation) => { + setSelectedInstallation(installation); + setBinaryPathChanged(installation.path !== currentBinaryPath); + }; return (
@@ -391,6 +426,25 @@ export const Settings: React.FC = ({ How long to retain chat transcripts locally (default: 30 days)

+ + {/* Claude Binary Path Selector */} +
+
+ +

+ Select which Claude Code installation to use +

+
+ + {binaryPathChanged && ( +

+ ⚠️ Claude binary path has been changed. Remember to save your settings. +

+ )} +
diff --git a/src/components/Topbar.tsx b/src/components/Topbar.tsx index 6a6af56..e28aec4 100644 --- a/src/components/Topbar.tsx +++ b/src/components/Topbar.tsx @@ -95,21 +95,28 @@ export const Topbar: React.FC = ({ if (!versionStatus) return null; const statusContent = ( -
- - - {versionStatus.is_installed && versionStatus.version - ? `Claude Code ${versionStatus.version}` - : "Claude Code"} - -
+ ); if (!versionStatus.is_installed) { @@ -124,6 +131,14 @@ export const Topbar: React.FC = ({ {versionStatus.output} + , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ); +}); +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; + +export { RadioGroup, RadioGroupItem }; \ No newline at end of file diff --git a/src/lib/api.ts b/src/lib/api.ts index eb31f34..3d5f2dd 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -78,6 +78,18 @@ export interface FileEntry { extension?: string; } +/** + * Represents a Claude installation found on the system + */ +export interface ClaudeInstallation { + /** Full path to the Claude binary */ + path: string; + /** Version string if available */ + version?: string; + /** Source of discovery (e.g., "nvm", "system", "homebrew", "which") */ + source: string; +} + // Sandbox API types export interface SandboxProfile { id?: number; @@ -1908,4 +1920,17 @@ export const api = { throw error; } }, + + /** + * List all available Claude installations on the system + * @returns Promise resolving to an array of Claude installations + */ + async listClaudeInstallations(): Promise { + try { + return await invoke("list_claude_installations"); + } catch (error) { + console.error("Failed to list Claude installations:", error); + throw error; + } + }, };