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
This commit is contained in:
@@ -9,6 +9,8 @@ use std::time::SystemTime;
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tokio::process::{Child, Command};
|
||||
use tokio::sync::Mutex;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tauri_plugin_shell::process::CommandEvent;
|
||||
|
||||
/// Global state to track current Claude process
|
||||
pub struct ClaudeProcessState {
|
||||
@@ -263,6 +265,51 @@ fn create_command_with_env(program: &str) -> Command {
|
||||
tokio_cmd
|
||||
}
|
||||
|
||||
/// Determines whether to use sidecar or system binary execution
|
||||
fn should_use_sidecar(claude_path: &str) -> bool {
|
||||
claude_path == "claude-code"
|
||||
}
|
||||
|
||||
/// Creates a sidecar command with the given arguments
|
||||
fn create_sidecar_command(
|
||||
app: &AppHandle,
|
||||
args: Vec<String>,
|
||||
project_path: &str,
|
||||
) -> Result<tauri_plugin_shell::process::Command, String> {
|
||||
let mut sidecar_cmd = app
|
||||
.shell()
|
||||
.sidecar("claude-code")
|
||||
.map_err(|e| format!("Failed to create sidecar command: {}", e))?;
|
||||
|
||||
// Add all arguments
|
||||
sidecar_cmd = sidecar_cmd.args(args);
|
||||
|
||||
// Set working directory
|
||||
sidecar_cmd = sidecar_cmd.current_dir(project_path);
|
||||
|
||||
Ok(sidecar_cmd)
|
||||
}
|
||||
|
||||
/// Creates a system binary command with the given arguments
|
||||
fn create_system_command(
|
||||
claude_path: &str,
|
||||
args: Vec<String>,
|
||||
project_path: &str,
|
||||
) -> Command {
|
||||
let mut cmd = create_command_with_env(claude_path);
|
||||
|
||||
// Add all arguments
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
cmd.current_dir(project_path)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Lists all projects in the ~/.claude/projects directory
|
||||
#[tauri::command]
|
||||
pub async fn list_projects() -> Result<Vec<Project>, String> {
|
||||
@@ -530,6 +577,21 @@ pub async fn check_claude_version(app: AppHandle) -> Result<ClaudeVersionStatus,
|
||||
}
|
||||
};
|
||||
|
||||
// If the selected path is the special sidecar identifier, we cannot execute it directly.
|
||||
// Instead, assume the bundled sidecar is available (find_claude_binary already verified
|
||||
// this) and return a positive status without a version string. Attempting to spawn the
|
||||
// sidecar here would require async streaming plumbing that is over-kill for a simple
|
||||
// presence check and fails in debug builds (os error 2).
|
||||
if claude_path == "claude-code" {
|
||||
return Ok(ClaudeVersionStatus {
|
||||
is_installed: true,
|
||||
version: None,
|
||||
output: "Using bundled Claude Code sidecar".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
use log::debug;debug!("Claude path: {}", claude_path);
|
||||
|
||||
// In production builds, we can't check the version directly
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
@@ -660,15 +722,15 @@ fn find_claude_md_recursive(
|
||||
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
|
||||
let path = entry.path();
|
||||
|
||||
// Skip hidden directories and files
|
||||
// Skip hidden files/directories
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if name.starts_with('.') && name != ".claude" {
|
||||
if name.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
// Skip common directories that shouldn't be scanned
|
||||
// Skip common directories that shouldn't be searched
|
||||
if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if matches!(
|
||||
dir_name,
|
||||
@@ -678,7 +740,6 @@ fn find_claude_md_recursive(
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into subdirectory
|
||||
find_claude_md_recursive(&path, project_root, claude_files)?;
|
||||
} else if path.is_file() {
|
||||
// Check if it's a CLAUDE.md file (case insensitive)
|
||||
@@ -799,21 +860,24 @@ pub async fn execute_claude_code(
|
||||
);
|
||||
|
||||
let claude_path = find_claude_binary(&app)?;
|
||||
let mut cmd = create_command_with_env(&claude_path);
|
||||
|
||||
let args = vec![
|
||||
"-p".to_string(),
|
||||
prompt.clone(),
|
||||
"--model".to_string(),
|
||||
model.clone(),
|
||||
"--output-format".to_string(),
|
||||
"stream-json".to_string(),
|
||||
"--verbose".to_string(),
|
||||
"--dangerously-skip-permissions".to_string(),
|
||||
];
|
||||
|
||||
cmd.arg("-p")
|
||||
.arg(&prompt)
|
||||
.arg("--model")
|
||||
.arg(&model)
|
||||
.arg("--output-format")
|
||||
.arg("stream-json")
|
||||
.arg("--verbose")
|
||||
.arg("--dangerously-skip-permissions")
|
||||
.current_dir(&project_path)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
spawn_claude_process(app, cmd, prompt, model, project_path).await
|
||||
if should_use_sidecar(&claude_path) {
|
||||
spawn_claude_sidecar(app, args, prompt, model, project_path).await
|
||||
} else {
|
||||
let cmd = create_system_command(&claude_path, args, &project_path);
|
||||
spawn_claude_process(app, cmd, prompt, model, project_path).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Continue an existing Claude Code conversation with streaming output
|
||||
@@ -831,22 +895,25 @@ pub async fn continue_claude_code(
|
||||
);
|
||||
|
||||
let claude_path = find_claude_binary(&app)?;
|
||||
let mut cmd = create_command_with_env(&claude_path);
|
||||
|
||||
let args = vec![
|
||||
"-c".to_string(), // Continue flag
|
||||
"-p".to_string(),
|
||||
prompt.clone(),
|
||||
"--model".to_string(),
|
||||
model.clone(),
|
||||
"--output-format".to_string(),
|
||||
"stream-json".to_string(),
|
||||
"--verbose".to_string(),
|
||||
"--dangerously-skip-permissions".to_string(),
|
||||
];
|
||||
|
||||
cmd.arg("-c") // Continue flag
|
||||
.arg("-p")
|
||||
.arg(&prompt)
|
||||
.arg("--model")
|
||||
.arg(&model)
|
||||
.arg("--output-format")
|
||||
.arg("stream-json")
|
||||
.arg("--verbose")
|
||||
.arg("--dangerously-skip-permissions")
|
||||
.current_dir(&project_path)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
spawn_claude_process(app, cmd, prompt, model, project_path).await
|
||||
if should_use_sidecar(&claude_path) {
|
||||
spawn_claude_sidecar(app, args, prompt, model, project_path).await
|
||||
} else {
|
||||
let cmd = create_system_command(&claude_path, args, &project_path);
|
||||
spawn_claude_process(app, cmd, prompt, model, project_path).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Resume an existing Claude Code session by ID with streaming output
|
||||
@@ -866,23 +933,26 @@ pub async fn resume_claude_code(
|
||||
);
|
||||
|
||||
let claude_path = find_claude_binary(&app)?;
|
||||
let mut cmd = create_command_with_env(&claude_path);
|
||||
|
||||
let args = vec![
|
||||
"--resume".to_string(),
|
||||
session_id.clone(),
|
||||
"-p".to_string(),
|
||||
prompt.clone(),
|
||||
"--model".to_string(),
|
||||
model.clone(),
|
||||
"--output-format".to_string(),
|
||||
"stream-json".to_string(),
|
||||
"--verbose".to_string(),
|
||||
"--dangerously-skip-permissions".to_string(),
|
||||
];
|
||||
|
||||
cmd.arg("--resume")
|
||||
.arg(&session_id)
|
||||
.arg("-p")
|
||||
.arg(&prompt)
|
||||
.arg("--model")
|
||||
.arg(&model)
|
||||
.arg("--output-format")
|
||||
.arg("stream-json")
|
||||
.arg("--verbose")
|
||||
.arg("--dangerously-skip-permissions")
|
||||
.current_dir(&project_path)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
spawn_claude_process(app, cmd, prompt, model, project_path).await
|
||||
if should_use_sidecar(&claude_path) {
|
||||
spawn_claude_sidecar(app, args, prompt, model, project_path).await
|
||||
} else {
|
||||
let cmd = create_system_command(&claude_path, args, &project_path);
|
||||
spawn_claude_process(app, cmd, prompt, model, project_path).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel the currently running Claude Code execution
|
||||
@@ -1031,9 +1101,6 @@ pub async fn get_claude_session_output(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// Helper function to spawn Claude process and handle streaming
|
||||
async fn spawn_claude_process(app: AppHandle, mut cmd: Command, prompt: String, model: String, project_path: String) -> Result<(), String> {
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
@@ -1202,6 +1269,145 @@ async fn spawn_claude_process(app: AppHandle, mut cmd: Command, prompt: String,
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to spawn Claude sidecar process and handle streaming
|
||||
async fn spawn_claude_sidecar(
|
||||
app: AppHandle,
|
||||
args: Vec<String>,
|
||||
prompt: String,
|
||||
model: String,
|
||||
project_path: String,
|
||||
) -> Result<(), String> {
|
||||
use std::sync::Mutex;
|
||||
|
||||
// Create the sidecar command
|
||||
let sidecar_cmd = create_sidecar_command(&app, args, &project_path)?;
|
||||
|
||||
// Spawn the sidecar process
|
||||
let (mut rx, child) = sidecar_cmd
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to spawn Claude sidecar: {}", e))?;
|
||||
|
||||
// Get the child PID for logging
|
||||
let pid = child.pid();
|
||||
log::info!("Spawned Claude sidecar process with PID: {:?}", pid);
|
||||
|
||||
// We'll extract the session ID from Claude's init message
|
||||
let session_id_holder: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
|
||||
let run_id_holder: Arc<Mutex<Option<i64>>> = Arc::new(Mutex::new(None));
|
||||
|
||||
// Register with ProcessRegistry
|
||||
let registry = app.state::<crate::process::ProcessRegistryState>();
|
||||
let registry_clone = registry.0.clone();
|
||||
let project_path_clone = project_path.clone();
|
||||
let prompt_clone = prompt.clone();
|
||||
let model_clone = model.clone();
|
||||
|
||||
// Spawn task to read events from sidecar
|
||||
let app_handle = app.clone();
|
||||
let session_id_holder_clone = session_id_holder.clone();
|
||||
let run_id_holder_clone = run_id_holder.clone();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line_bytes) => {
|
||||
let line = String::from_utf8_lossy(&line_bytes);
|
||||
let line_str = line.trim_end_matches('\n').trim_end_matches('\r');
|
||||
|
||||
if !line_str.is_empty() {
|
||||
log::debug!("Claude sidecar stdout: {}", line_str);
|
||||
|
||||
// Parse the line to check for init message with session ID
|
||||
if let Ok(msg) = serde_json::from_str::<serde_json::Value>(line_str) {
|
||||
if msg["type"] == "system" && msg["subtype"] == "init" {
|
||||
if let Some(claude_session_id) = msg["session_id"].as_str() {
|
||||
let mut session_id_guard = session_id_holder_clone.lock().unwrap();
|
||||
if session_id_guard.is_none() {
|
||||
*session_id_guard = Some(claude_session_id.to_string());
|
||||
log::info!("Extracted Claude session ID: {}", claude_session_id);
|
||||
|
||||
// Register with ProcessRegistry using Claude's session ID
|
||||
match registry_clone.register_claude_session(
|
||||
claude_session_id.to_string(),
|
||||
pid,
|
||||
project_path_clone.clone(),
|
||||
prompt_clone.clone(),
|
||||
model_clone.clone(),
|
||||
) {
|
||||
Ok(run_id) => {
|
||||
log::info!("Registered Claude sidecar session with run_id: {}", run_id);
|
||||
let mut run_id_guard = run_id_holder_clone.lock().unwrap();
|
||||
*run_id_guard = Some(run_id);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to register Claude sidecar session: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store live output in registry if we have a run_id
|
||||
if let Some(run_id) = *run_id_holder_clone.lock().unwrap() {
|
||||
let _ = registry_clone.append_live_output(run_id, line_str);
|
||||
}
|
||||
|
||||
// Emit the line to the frontend with session isolation if we have session ID
|
||||
if let Some(ref session_id) = *session_id_holder_clone.lock().unwrap() {
|
||||
let _ = app_handle.emit(&format!("claude-output:{}", session_id), line_str);
|
||||
}
|
||||
// Also emit to the generic event for backward compatibility
|
||||
let _ = app_handle.emit("claude-output", line_str);
|
||||
}
|
||||
}
|
||||
CommandEvent::Stderr(line_bytes) => {
|
||||
let line = String::from_utf8_lossy(&line_bytes);
|
||||
let line_str = line.trim_end_matches('\n').trim_end_matches('\r');
|
||||
|
||||
if !line_str.is_empty() {
|
||||
log::error!("Claude sidecar stderr: {}", line_str);
|
||||
|
||||
// Emit error lines to the frontend with session isolation if we have session ID
|
||||
if let Some(ref session_id) = *session_id_holder_clone.lock().unwrap() {
|
||||
let _ = app_handle.emit(&format!("claude-error:{}", session_id), line_str);
|
||||
}
|
||||
// Also emit to the generic event for backward compatibility
|
||||
let _ = app_handle.emit("claude-error", line_str);
|
||||
}
|
||||
}
|
||||
CommandEvent::Terminated(payload) => {
|
||||
log::info!("Claude sidecar process terminated with payload: {:?}", payload);
|
||||
|
||||
// Add a small delay to ensure all messages are processed
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
|
||||
let success = payload.code.unwrap_or(-1) == 0;
|
||||
|
||||
if let Some(ref session_id) = *session_id_holder_clone.lock().unwrap() {
|
||||
let _ = app_handle.emit(&format!("claude-complete:{}", session_id), success);
|
||||
}
|
||||
// Also emit to the generic event for backward compatibility
|
||||
let _ = app_handle.emit("claude-complete", success);
|
||||
|
||||
// Unregister from ProcessRegistry if we have a run_id
|
||||
if let Some(run_id) = *run_id_holder_clone.lock().unwrap() {
|
||||
let _ = registry_clone.unregister_process(run_id);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
// Handle other event types if needed
|
||||
log::debug!("Claude sidecar event: {:?}", event);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lists files and directories in a given path
|
||||
#[tauri::command]
|
||||
pub async fn list_directory_contents(directory_path: String) -> Result<Vec<FileEntry>, String> {
|
||||
@@ -1905,4 +2111,3 @@ pub async fn track_session_messages(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user