Skip to content

Commit a66697a

Browse files
committed
feat: implement sidecar binary support and enhance Claude execution system
- **Enhanced Claude Binary Management**: Added support for sidecar binary execution alongside system binaries - **Improved Command Creation**: Refactored command creation logic with separate functions for sidecar and system binaries - **Enhanced Process Management**: Better process lifecycle management with improved error handling - **Updated Tauri Configuration**: Added shell plugin configuration and expanded security policies - **Agent Commands**: Enhanced agent management with improved error handling and validation - **Redesigned Claude Version Selector**: Complete UI overhaul with modern select component and better UX - **Enhanced Settings Integration**: Improved settings page integration with new selector component - **API Layer Updates**: Updated API calls to support new binary execution modes - **UI Component Improvements**: Better visual feedback and loading states - **Updated Capabilities**: Enhanced Tauri capabilities for better security and functionality - **Documentation Updates**: Updated scripts README with new build instructions - **Security Enhancements**: Improved CSP policies and asset protocol configuration - Added function to determine execution mode - Implemented for sidecar binary execution - Implemented for system binary execution - Enhanced process management with better error handling - Replaced radio group with modern select component - Added visual indicators for different installation types - Improved loading states and error feedback - Better responsive design and accessibility - Enhanced CSP policies for better security - Improved asset protocol configuration - Better error handling and validation throughout - Optimized process management and resource usage - 10 files modified with 647 additions and 208 deletions - Major changes in Claude execution system and UI components - Configuration updates for enhanced security and functionality - All existing functionality preserved - New sidecar binary support tested - UI components thoroughly tested for accessibility and responsiveness
1 parent 9115153 commit a66697a

10 files changed

Lines changed: 647 additions & 208 deletions

File tree

scripts/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ All executables are created in the `src-tauri/binaries/` directory with the foll
7676
- **Cross-platform**: Supports all major operating systems and architectures
7777
- **CPU Variants**: Modern variants for newer CPUs (2013+), baseline for compatibility
7878
- **Self-contained**: No external dependencies required at runtime
79+
- **Tauri Integration**: Automatic sidecar binary naming for seamless Tauri integration
7980

8081
## Requirements
8182

src-tauri/capabilities/default.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,26 @@
1111
"shell:allow-execute",
1212
"shell:allow-spawn",
1313
"shell:allow-open",
14+
{
15+
"identifier": "shell:allow-execute",
16+
"allow": [
17+
{
18+
"name": "claude-code",
19+
"sidecar": true,
20+
"args": true
21+
}
22+
]
23+
},
24+
{
25+
"identifier": "shell:allow-spawn",
26+
"allow": [
27+
{
28+
"name": "claude-code",
29+
"sidecar": true,
30+
"args": true
31+
}
32+
]
33+
},
1434
"fs:default",
1535
"fs:allow-mkdir",
1636
"fs:allow-read",

src-tauri/src/claude_binary.rs

Lines changed: 109 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,93 @@ use log::{debug, error, info, warn};
33
use serde::{Deserialize, Serialize};
44
use std::cmp::Ordering;
55
/// Shared module for detecting Claude Code binary installations
6-
/// Supports NVM installations, aliased paths, and version-based selection
6+
/// Supports NVM installations, aliased paths, version-based selection, and bundled sidecars
77
use std::path::PathBuf;
88
use std::process::Command;
99
use tauri::Manager;
1010

11+
/// Type of Claude installation
12+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13+
pub enum InstallationType {
14+
/// Bundled sidecar binary (preferred)
15+
Bundled,
16+
/// System-installed binary
17+
System,
18+
/// Custom path specified by user
19+
Custom,
20+
}
21+
1122
/// Represents a Claude installation with metadata
1223
#[derive(Debug, Clone, Serialize, Deserialize)]
1324
pub struct ClaudeInstallation {
14-
/// Full path to the Claude binary
25+
/// Full path to the Claude binary (or "claude-code" for sidecar)
1526
pub path: String,
1627
/// Version string if available
1728
pub version: Option<String>,
18-
/// Source of discovery (e.g., "nvm", "system", "homebrew", "which")
29+
/// Source of discovery (e.g., "nvm", "system", "homebrew", "which", "bundled")
1930
pub source: String,
31+
/// Type of installation
32+
pub installation_type: InstallationType,
2033
}
2134

2235
/// Main function to find the Claude binary
23-
/// Checks database first, then discovers all installations and selects the best one
36+
/// Checks database first for stored path and preference, then prioritizes accordingly
2437
pub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result<String, String> {
2538
info!("Searching for claude binary...");
2639

27-
// First check if we have a stored path in the database
40+
// First check if we have a stored path and preference in the database
2841
if let Ok(app_data_dir) = app_handle.path().app_data_dir() {
2942
let db_path = app_data_dir.join("agents.db");
3043
if db_path.exists() {
3144
if let Ok(conn) = rusqlite::Connection::open(&db_path) {
45+
// Check for stored path first
3246
if let Ok(stored_path) = conn.query_row(
3347
"SELECT value FROM app_settings WHERE key = 'claude_binary_path'",
3448
[],
3549
|row| row.get::<_, String>(0),
3650
) {
3751
info!("Found stored claude path in database: {}", stored_path);
52+
53+
// If it's a sidecar reference, return it directly
54+
if stored_path == "claude-code" {
55+
return Ok(stored_path);
56+
}
57+
58+
// Otherwise check if the path still exists
3859
let path_buf = PathBuf::from(&stored_path);
3960
if path_buf.exists() && path_buf.is_file() {
4061
return Ok(stored_path);
4162
} else {
4263
warn!("Stored claude path no longer exists: {}", stored_path);
4364
}
4465
}
66+
67+
// Check user preference
68+
let preference = conn.query_row(
69+
"SELECT value FROM app_settings WHERE key = 'claude_installation_preference'",
70+
[],
71+
|row| row.get::<_, String>(0),
72+
).unwrap_or_else(|_| "bundled".to_string());
73+
74+
info!("User preference for Claude installation: {}", preference);
75+
76+
// If user prefers bundled and it's available, use it
77+
if preference == "bundled" && is_sidecar_available(app_handle) {
78+
info!("Using bundled Claude Code sidecar per user preference");
79+
return Ok("claude-code".to_string());
80+
}
4581
}
4682
}
4783
}
4884

49-
// Discover all available installations
50-
let installations = discover_all_installations();
85+
// Check for bundled sidecar (if no preference or bundled preferred)
86+
if is_sidecar_available(app_handle) {
87+
info!("Found bundled Claude Code sidecar");
88+
return Ok("claude-code".to_string());
89+
}
90+
91+
// Discover all available system installations
92+
let installations = discover_system_installations();
5193

5294
if installations.is_empty() {
5395
error!("Could not find claude binary in any location");
@@ -71,39 +113,77 @@ pub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result<String, Strin
71113
}
72114
}
73115

116+
/// Check if the bundled sidecar is available
117+
fn is_sidecar_available(app_handle: &tauri::AppHandle) -> bool {
118+
// Try to create a sidecar command to test availability
119+
use tauri_plugin_shell::ShellExt;
120+
121+
match app_handle.shell().sidecar("claude-code") {
122+
Ok(_) => {
123+
debug!("Bundled Claude Code sidecar is available");
124+
true
125+
}
126+
Err(e) => {
127+
debug!("Bundled Claude Code sidecar not available: {}", e);
128+
false
129+
}
130+
}
131+
}
132+
74133
/// Discovers all available Claude installations and returns them for selection
75134
/// This allows UI to show a version selector
76135
pub fn discover_claude_installations() -> Vec<ClaudeInstallation> {
77136
info!("Discovering all Claude installations...");
78137

79-
let installations = discover_all_installations();
138+
let mut installations = Vec::new();
80139

81-
// Sort by version (highest first), then by source preference
82-
let mut sorted = installations;
83-
sorted.sort_by(|a, b| {
84-
match (&a.version, &b.version) {
85-
(Some(v1), Some(v2)) => {
86-
// Compare versions in descending order (newest first)
87-
match compare_versions(v2, v1) {
88-
Ordering::Equal => {
89-
// If versions are equal, prefer by source
90-
source_preference(a).cmp(&source_preference(b))
140+
// Always add bundled sidecar as first option if available
141+
// We can't easily check version for sidecar without spawning it, so we'll mark it as bundled
142+
installations.push(ClaudeInstallation {
143+
path: "claude-code".to_string(),
144+
version: None, // Version will be determined at runtime
145+
source: "bundled".to_string(),
146+
installation_type: InstallationType::Bundled,
147+
});
148+
149+
// Add system installations
150+
installations.extend(discover_system_installations());
151+
152+
// Sort by installation type (Bundled first), then by version (highest first), then by source preference
153+
installations.sort_by(|a, b| {
154+
// First sort by installation type (Bundled comes first)
155+
match (&a.installation_type, &b.installation_type) {
156+
(InstallationType::Bundled, InstallationType::Bundled) => Ordering::Equal,
157+
(InstallationType::Bundled, _) => Ordering::Less,
158+
(_, InstallationType::Bundled) => Ordering::Greater,
159+
_ => {
160+
// For non-bundled installations, sort by version then source
161+
match (&a.version, &b.version) {
162+
(Some(v1), Some(v2)) => {
163+
// Compare versions in descending order (newest first)
164+
match compare_versions(v2, v1) {
165+
Ordering::Equal => {
166+
// If versions are equal, prefer by source
167+
source_preference(a).cmp(&source_preference(b))
168+
}
169+
other => other,
170+
}
91171
}
92-
other => other,
172+
(Some(_), None) => Ordering::Less, // Version comes before no version
173+
(None, Some(_)) => Ordering::Greater,
174+
(None, None) => source_preference(a).cmp(&source_preference(b)),
93175
}
94176
}
95-
(Some(_), None) => Ordering::Less, // Version comes before no version
96-
(None, Some(_)) => Ordering::Greater,
97-
(None, None) => source_preference(a).cmp(&source_preference(b)),
98177
}
99178
});
100179

101-
sorted
180+
installations
102181
}
103182

104183
/// Returns a preference score for installation sources (lower is better)
105184
fn source_preference(installation: &ClaudeInstallation) -> u8 {
106185
match installation.source.as_str() {
186+
"bundled" => 0, // Bundled sidecar has highest preference
107187
"which" => 1,
108188
"homebrew" => 2,
109189
"system" => 3,
@@ -120,8 +200,8 @@ fn source_preference(installation: &ClaudeInstallation) -> u8 {
120200
}
121201
}
122202

123-
/// Discovers all Claude installations on the system
124-
fn discover_all_installations() -> Vec<ClaudeInstallation> {
203+
/// Discovers all Claude system installations on the system (excludes bundled sidecar)
204+
fn discover_system_installations() -> Vec<ClaudeInstallation> {
125205
let mut installations = Vec::new();
126206

127207
// 1. Try 'which' command first (now works in production)
@@ -179,6 +259,7 @@ fn try_which_command() -> Option<ClaudeInstallation> {
179259
path,
180260
version,
181261
source: "which".to_string(),
262+
installation_type: InstallationType::System,
182263
})
183264
}
184265
_ => None,
@@ -215,6 +296,7 @@ fn find_nvm_installations() -> Vec<ClaudeInstallation> {
215296
path: path_str,
216297
version,
217298
source: format!("nvm ({})", node_version),
299+
installation_type: InstallationType::System,
218300
});
219301
}
220302
}
@@ -283,6 +365,7 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {
283365
path,
284366
version,
285367
source,
368+
installation_type: InstallationType::System,
286369
});
287370
}
288371
}
@@ -297,6 +380,7 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {
297380
path: "claude".to_string(),
298381
version,
299382
source: "PATH".to_string(),
383+
installation_type: InstallationType::System,
300384
});
301385
}
302386
}

src-tauri/src/commands/agents.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1392,7 +1392,20 @@ pub async fn get_claude_binary_path(db: State<'_, AgentDb>) -> Result<Option<Str
13921392
pub async fn set_claude_binary_path(db: State<'_, AgentDb>, path: String) -> Result<(), String> {
13931393
let conn = db.0.lock().map_err(|e| e.to_string())?;
13941394

1395-
// Validate that the path exists and is executable
1395+
// Special handling for bundled sidecar reference
1396+
if path == "claude-code" {
1397+
// For bundled sidecar, we don't need to validate file existence
1398+
// as it's handled by Tauri's sidecar system
1399+
conn.execute(
1400+
"INSERT INTO app_settings (key, value) VALUES ('claude_binary_path', ?1)
1401+
ON CONFLICT(key) DO UPDATE SET value = ?1",
1402+
params![path],
1403+
)
1404+
.map_err(|e| format!("Failed to save Claude binary path: {}", e))?;
1405+
return Ok(());
1406+
}
1407+
1408+
// Validate that the path exists and is executable for system installations
13961409
let path_buf = std::path::PathBuf::from(&path);
13971410
if !path_buf.exists() {
13981411
return Err(format!("File does not exist: {}", path));
@@ -1489,6 +1502,26 @@ fn create_command_with_env(program: &str) -> Command {
14891502
tokio_cmd.env("PATH", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin");
14901503
}
14911504

1505+
// BEGIN PATCH: Ensure bundled sidecar directory is in PATH when using the "claude-code" placeholder
1506+
if program == "claude-code" {
1507+
// Attempt to locate the sidecar binaries directory that Tauri uses during development
1508+
// At compile-time, CARGO_MANIFEST_DIR resolves to the absolute path of the src-tauri crate.
1509+
// The sidecar binaries live in <src-tauri>/binaries.
1510+
#[allow(clippy::redundant_clone)]
1511+
let sidecar_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("binaries");
1512+
if sidecar_dir.exists() {
1513+
if let Some(sidecar_dir_str) = sidecar_dir.to_str() {
1514+
let current_path = std::env::var("PATH").unwrap_or_default();
1515+
let separator = if cfg!(target_os = "windows") { ";" } else { ":" };
1516+
if !current_path.split(separator).any(|p| p == sidecar_dir_str) {
1517+
let new_path = format!("{}{}{}", sidecar_dir_str, separator, current_path);
1518+
tokio_cmd.env("PATH", new_path);
1519+
}
1520+
}
1521+
}
1522+
}
1523+
// END PATCH
1524+
14921525
tokio_cmd
14931526
}
14941527

0 commit comments

Comments
 (0)