refactor: remove bundled Claude Code binary support

- Remove all bundled/sidecar binary functionality
- Delete build scripts for fetching and building Claude executables
- Simplify binary detection to only support system installations
- Update UI to remove bundled installation options
- Update build configuration and documentation
This commit is contained in:
Vivek R
2025-07-22 21:16:20 +05:30
parent abc7323194
commit 4ddb6a1995
15 changed files with 40 additions and 1568 deletions

View File

@@ -14,11 +14,6 @@
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "claude-code",
"sidecar": true,
"args": true
},
{
"name": "claude",
"sidecar": false,
@@ -29,11 +24,6 @@
{
"identifier": "shell:allow-spawn",
"allow": [
{
"name": "claude-code",
"sidecar": true,
"args": true
},
{
"name": "claude",
"sidecar": false,

View File

@@ -3,7 +3,7 @@ use log::{debug, error, info, warn};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
/// Shared module for detecting Claude Code binary installations
/// Supports NVM installations, aliased paths, version-based selection, and bundled sidecars
/// Supports NVM installations, aliased paths, and version-based selection
use std::path::PathBuf;
use std::process::Command;
use tauri::Manager;
@@ -11,8 +11,6 @@ use tauri::Manager;
/// Type of Claude installation
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum InstallationType {
/// Bundled sidecar binary (preferred)
Bundled,
/// System-installed binary
System,
/// Custom path specified by user
@@ -22,11 +20,11 @@ pub enum InstallationType {
/// Represents a Claude installation with metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaudeInstallation {
/// Full path to the Claude binary (or "claude-code" for sidecar)
/// Full path to the Claude binary
pub path: String,
/// Version string if available
pub version: Option<String>,
/// Source of discovery (e.g., "nvm", "system", "homebrew", "which", "bundled")
/// Source of discovery (e.g., "nvm", "system", "homebrew", "which")
pub source: String,
/// Type of installation
pub installation_type: InstallationType,
@@ -50,12 +48,7 @@ pub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result<String, Strin
) {
info!("Found stored claude path in database: {}", stored_path);
// If it's a sidecar reference, return it directly
if stored_path == "claude-code" {
return Ok(stored_path);
}
// Otherwise check if the path still exists
// Check if the path still exists
let path_buf = PathBuf::from(&stored_path);
if path_buf.exists() && path_buf.is_file() {
return Ok(stored_path);
@@ -69,25 +62,13 @@ pub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result<String, Strin
"SELECT value FROM app_settings WHERE key = 'claude_installation_preference'",
[],
|row| row.get::<_, String>(0),
).unwrap_or_else(|_| "bundled".to_string());
).unwrap_or_else(|_| "system".to_string());
info!("User preference for Claude installation: {}", preference);
// If user prefers bundled and it's available, use it
if preference == "bundled" && is_sidecar_available(app_handle) {
info!("Using bundled Claude Code sidecar per user preference");
return Ok("claude-code".to_string());
}
}
}
}
// Check for bundled sidecar (if no preference or bundled preferred)
if is_sidecar_available(app_handle) {
info!("Found bundled Claude Code sidecar");
return Ok("claude-code".to_string());
}
// Discover all available system installations
let installations = discover_system_installations();
@@ -113,67 +94,29 @@ pub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result<String, Strin
}
}
/// Check if the bundled sidecar is available
fn is_sidecar_available(app_handle: &tauri::AppHandle) -> bool {
// Try to create a sidecar command to test availability
use tauri_plugin_shell::ShellExt;
match app_handle.shell().sidecar("claude-code") {
Ok(_) => {
debug!("Bundled Claude Code sidecar is available");
true
}
Err(e) => {
debug!("Bundled Claude Code sidecar not available: {}", e);
false
}
}
}
/// 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 mut installations = Vec::new();
let mut installations = discover_system_installations();
// Always add bundled sidecar as first option if available
// We can't easily check version for sidecar without spawning it, so we'll mark it as bundled
installations.push(ClaudeInstallation {
path: "claude-code".to_string(),
version: None, // Version will be determined at runtime
source: "bundled".to_string(),
installation_type: InstallationType::Bundled,
});
// Add system installations
installations.extend(discover_system_installations());
// Sort by installation type (Bundled first), then by version (highest first), then by source preference
// Sort by version (highest first), then by source preference
installations.sort_by(|a, b| {
// First sort by installation type (Bundled comes first)
match (&a.installation_type, &b.installation_type) {
(InstallationType::Bundled, InstallationType::Bundled) => Ordering::Equal,
(InstallationType::Bundled, _) => Ordering::Less,
(_, InstallationType::Bundled) => Ordering::Greater,
_ => {
// For non-bundled installations, sort by version then source
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,
}
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))
}
(Some(_), None) => Ordering::Less, // Version comes before no version
(None, Some(_)) => Ordering::Greater,
(None, None) => 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)),
}
});
@@ -183,7 +126,6 @@ pub fn discover_claude_installations() -> Vec<ClaudeInstallation> {
/// Returns a preference score for installation sources (lower is better)
fn source_preference(installation: &ClaudeInstallation) -> u8 {
match installation.source.as_str() {
"bundled" => 0, // Bundled sidecar has highest preference
"which" => 1,
"homebrew" => 2,
"system" => 3,
@@ -200,7 +142,7 @@ fn source_preference(installation: &ClaudeInstallation) -> u8 {
}
}
/// Discovers all Claude system installations on the system (excludes bundled sidecar)
/// Discovers all Claude installations on the system
fn discover_system_installations() -> Vec<ClaudeInstallation> {
let mut installations = Vec::new();

View File

@@ -766,37 +766,8 @@ pub async fn execute_agent(
"--dangerously-skip-permissions".to_string(),
];
// Execute based on whether we should use sidecar or system binary
if should_use_sidecar(&claude_path) {
spawn_agent_sidecar(app, run_id, agent_id, agent.name.clone(), args, project_path, task, execution_model, db, registry).await
} else {
spawn_agent_system(app, run_id, agent_id, agent.name.clone(), claude_path, args, project_path, task, execution_model, db, registry).await
}
}
/// Determines whether to use sidecar or system binary execution for agents
fn should_use_sidecar(claude_path: &str) -> bool {
claude_path == "claude-code"
}
/// Creates a sidecar command for agent execution
fn create_agent_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)
// Execute using system binary
spawn_agent_system(app, run_id, agent_id, agent.name.clone(), claude_path, args, project_path, task, execution_model, db, registry).await
}
/// Creates a system binary command for agent execution
@@ -821,239 +792,6 @@ fn create_agent_system_command(
}
/// Spawn agent using sidecar command
async fn spawn_agent_sidecar(
app: AppHandle,
run_id: i64,
_agent_id: i64,
_agent_name: String,
args: Vec<String>,
project_path: String,
_task: String,
_execution_model: String,
db: State<'_, AgentDb>,
registry: State<'_, crate::process::ProcessRegistryState>,
) -> Result<i64, String> {
use std::sync::Mutex;
// Create the sidecar command
let sidecar_cmd = create_agent_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();
info!("✅ Spawned Claude sidecar process with PID: {:?}", pid);
// Update the database with PID and status
let now = chrono::Utc::now().to_rfc3339();
{
let conn = db.0.lock().map_err(|e| e.to_string())?;
conn.execute(
"UPDATE agent_runs SET status = 'running', pid = ?1, process_started_at = ?2 WHERE id = ?3",
params![pid as i64, now, run_id],
).map_err(|e| e.to_string())?;
info!("📝 Updated database with running status and 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));
// Create variables we need for the spawned task
let app_dir = app
.path()
.app_data_dir()
.expect("Failed to get app data dir");
let db_path = app_dir.join("agents.db");
let db_path_for_stream = db_path.clone(); // Clone for the streaming task
// Spawn task to read events from sidecar
let app_handle = app.clone();
let session_id_holder_clone = session_id_holder.clone();
let live_output = std::sync::Arc::new(Mutex::new(String::new()));
let live_output_clone = live_output.clone();
let registry_clone = registry.0.clone();
let first_output = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let first_output_clone = first_output.clone();
let sidecar_task = tokio::spawn(async move {
info!("📖 Starting to read Claude sidecar events...");
let mut line_count = 0;
while let Some(event) = rx.recv().await {
match event {
tauri_plugin_shell::process::CommandEvent::Stdout(data) => {
let line = String::from_utf8_lossy(&data).trim().to_string();
if !line.is_empty() {
line_count += 1;
// Log first output
if !first_output_clone.load(std::sync::atomic::Ordering::Relaxed) {
info!("🎉 First output received from Claude sidecar! Line: {}", line);
first_output_clone.store(true, std::sync::atomic::Ordering::Relaxed);
}
if line_count <= 5 {
info!("sidecar stdout[{}]: {}", line_count, line);
} else {
debug!("sidecar stdout[{}]: {}", line_count, line);
}
// Store live output
if let Ok(mut output) = live_output_clone.lock() {
output.push_str(&line);
output.push('\n');
}
// Also store in process registry for cross-session access
let _ = registry_clone.append_live_output(run_id, &line);
// Extract session ID from JSONL output
if let Ok(json) = serde_json::from_str::<JsonValue>(&line) {
// Claude Code uses "session_id" (underscore), not "sessionId"
if json.get("type").and_then(|t| t.as_str()) == Some("system") &&
json.get("subtype").and_then(|s| s.as_str()) == Some("init") {
if let Some(sid) = json.get("session_id").and_then(|s| s.as_str()) {
if let Ok(mut current_session_id) = session_id_holder_clone.lock() {
if current_session_id.is_none() {
*current_session_id = Some(sid.to_string());
info!("🔑 Extracted session ID: {}", sid);
// Update database immediately with session ID
if let Ok(conn) = Connection::open(&db_path_for_stream) {
match conn.execute(
"UPDATE agent_runs SET session_id = ?1 WHERE id = ?2",
params![sid, run_id],
) {
Ok(rows) => {
if rows > 0 {
info!("✅ Updated agent run {} with session ID immediately", run_id);
}
}
Err(e) => {
error!("❌ Failed to update session ID immediately: {}", e);
}
}
}
}
}
}
}
}
// Emit the line to the frontend with run_id for isolation
let _ = app_handle.emit(&format!("agent-output:{}", run_id), &line);
// Also emit to the generic event for backward compatibility
let _ = app_handle.emit("agent-output", &line);
}
}
tauri_plugin_shell::process::CommandEvent::Stderr(data) => {
let line = String::from_utf8_lossy(&data).trim().to_string();
if !line.is_empty() {
error!("sidecar stderr: {}", line);
// Emit error lines to the frontend with run_id for isolation
let _ = app_handle.emit(&format!("agent-error:{}", run_id), &line);
// Also emit to the generic event for backward compatibility
let _ = app_handle.emit("agent-error", &line);
}
}
tauri_plugin_shell::process::CommandEvent::Terminated { .. } => {
info!("📖 Claude sidecar process terminated");
break;
}
tauri_plugin_shell::process::CommandEvent::Error(e) => {
error!("🔥 Claude sidecar error: {}", e);
break;
}
_ => {
// Handle any other event types we might not know about
debug!("Received unknown sidecar event type");
}
}
}
info!("📖 Finished reading Claude sidecar events. Total lines: {}", line_count);
});
// Monitor process status and wait for completion
tokio::spawn(async move {
info!("🕐 Starting sidecar process monitoring...");
// Wait for first output with timeout
for i in 0..300 {
// 30 seconds (300 * 100ms)
if first_output.load(std::sync::atomic::Ordering::Relaxed) {
info!("✅ Output detected after {}ms, continuing normal execution", i * 100);
break;
}
if i == 299 {
warn!("⏰ TIMEOUT: No output from Claude sidecar after 30 seconds");
warn!("💡 This usually means:");
warn!(" 1. Claude sidecar is waiting for user input");
warn!(" 2. Authentication issues (API key not found/invalid)");
warn!(" 3. Network connectivity issues");
warn!(" 4. Claude failed to initialize but didn't report an error");
// Update database with failed status
if let Ok(conn) = Connection::open(&db_path) {
let _ = conn.execute(
"UPDATE agent_runs SET status = 'failed', completed_at = CURRENT_TIMESTAMP WHERE id = ?1",
params![run_id],
);
}
let _ = app.emit("agent-complete", false);
let _ = app.emit(&format!("agent-complete:{}", run_id), false);
return;
}
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
// Wait for sidecar task to complete
info!("⏳ Waiting for sidecar reading to complete...");
let _ = sidecar_task.await;
// Get the session ID that was extracted
let extracted_session_id = if let Ok(Some(sid)) = session_id_holder.lock().map(|s| s.clone()) {
sid
} else {
String::new()
};
// Update the run record with session ID and mark as completed
if let Ok(conn) = Connection::open(&db_path) {
info!("🔄 Updating database with extracted session ID: {}", extracted_session_id);
match conn.execute(
"UPDATE agent_runs SET session_id = ?1, status = 'completed', completed_at = CURRENT_TIMESTAMP WHERE id = ?2",
params![extracted_session_id, run_id],
) {
Ok(rows_affected) => {
if rows_affected > 0 {
info!("✅ Successfully updated agent run {} with session ID: {}", run_id, extracted_session_id);
} else {
warn!("⚠️ No rows affected when updating agent run {} with session ID", run_id);
}
}
Err(e) => {
error!("❌ Failed to update agent run {} with session ID: {}", run_id, e);
}
}
} else {
error!("❌ Failed to open database to update session ID for run {}", run_id);
}
info!("✅ Claude sidecar execution monitoring complete");
let _ = app.emit("agent-complete", true);
let _ = app.emit(&format!("agent-complete:{}", run_id), true);
});
Ok(run_id)
}
/// Spawn agent using system binary command
async fn spawn_agent_system(
@@ -1830,20 +1568,7 @@ pub async fn get_claude_binary_path(db: State<'_, AgentDb>) -> Result<Option<Str
pub async fn set_claude_binary_path(db: State<'_, AgentDb>, path: String) -> Result<(), String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
// Special handling for bundled sidecar reference
if path == "claude-code" {
// For bundled sidecar, we don't need to validate file existence
// as it's handled by Tauri's sidecar system
conn.execute(
"INSERT INTO app_settings (key, value) VALUES ('claude_binary_path', ?1)
ON CONFLICT(key) DO UPDATE SET value = ?1",
params![path],
)
.map_err(|e| format!("Failed to save Claude binary path: {}", e))?;
return Ok(());
}
// Validate that the path exists and is executable for system installations
// Validate that the path exists and is executable
let path_buf = std::path::PathBuf::from(&path);
if !path_buf.exists() {
return Err(format!("File does not exist: {}", path));
@@ -1883,77 +1608,6 @@ pub async fn list_claude_installations(
return Err("No Claude Code installations found on the system".to_string());
}
// For bundled installations, execute the sidecar to get the actual version
for installation in &mut installations {
if installation.installation_type == crate::claude_binary::InstallationType::Bundled {
// Try to get the version by executing the sidecar
use tauri_plugin_shell::process::CommandEvent;
// Create a temporary directory for the sidecar to run in
let temp_dir = std::env::temp_dir();
// Create sidecar command with --version flag
let sidecar_cmd = match app
.shell()
.sidecar("claude-code") {
Ok(cmd) => cmd.args(["--version"]).current_dir(&temp_dir),
Err(e) => {
log::warn!("Failed to create sidecar command for version check: {}", e);
continue;
}
};
// Spawn the sidecar and collect output
match sidecar_cmd.spawn() {
Ok((mut rx, _child)) => {
let mut stdout_output = String::new();
let mut stderr_output = String::new();
// Set a timeout for version check
let timeout = tokio::time::Duration::from_secs(5);
let start_time = tokio::time::Instant::now();
while let Ok(Some(event)) = tokio::time::timeout_at(
start_time + timeout,
rx.recv()
).await {
match event {
CommandEvent::Stdout(data) => {
stdout_output.push_str(&String::from_utf8_lossy(&data));
}
CommandEvent::Stderr(data) => {
stderr_output.push_str(&String::from_utf8_lossy(&data));
}
CommandEvent::Terminated { .. } => {
break;
}
CommandEvent::Error(e) => {
log::warn!("Error during sidecar version check: {}", e);
break;
}
_ => {}
}
}
// Use regex to directly extract version pattern
let version_regex = regex::Regex::new(r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)").ok();
if let Some(regex) = version_regex {
if let Some(captures) = regex.captures(&stdout_output) {
if let Some(version_match) = captures.get(1) {
installation.version = Some(version_match.as_str().to_string());
log::info!("Bundled sidecar version: {}", version_match.as_str());
}
}
}
}
Err(e) => {
log::warn!("Failed to spawn sidecar for version check: {}", e);
}
}
}
}
Ok(installations)
}
@@ -2012,26 +1666,6 @@ fn create_command_with_env(program: &str) -> Command {
tokio_cmd.env("PATH", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin");
}
// BEGIN PATCH: Ensure bundled sidecar directory is in PATH when using the "claude-code" placeholder
if program == "claude-code" {
// Attempt to locate the sidecar binaries directory that Tauri uses during development
// At compile-time, CARGO_MANIFEST_DIR resolves to the absolute path of the src-tauri crate.
// The sidecar binaries live in <src-tauri>/binaries.
#[allow(clippy::redundant_clone)]
let sidecar_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("binaries");
if sidecar_dir.exists() {
if let Some(sidecar_dir_str) = sidecar_dir.to_str() {
let current_path = std::env::var("PATH").unwrap_or_default();
let separator = if cfg!(target_os = "windows") { ";" } else { ":" };
if !current_path.split(separator).any(|p| p == sidecar_dir_str) {
let new_path = format!("{}{}{}", sidecar_dir_str, separator, current_path);
tokio_cmd.env("PATH", new_path);
}
}
}
}
// END PATCH
tokio_cmd
}

View File

@@ -266,30 +266,6 @@ 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(
@@ -578,91 +554,6 @@ pub async fn check_claude_version(app: AppHandle) -> Result<ClaudeVersionStatus,
}
};
// If the selected path is the special sidecar identifier, execute it to get version
if claude_path == "claude-code" {
use tauri_plugin_shell::process::CommandEvent;
// Create a temporary directory for the sidecar to run in
let temp_dir = std::env::temp_dir();
// Create sidecar command with --version flag
let sidecar_cmd = match app
.shell()
.sidecar("claude-code") {
Ok(cmd) => cmd.args(["--version"]).current_dir(&temp_dir),
Err(e) => {
log::error!("Failed to create sidecar command: {}", e);
return Ok(ClaudeVersionStatus {
is_installed: true, // We know it exists, just couldn't create command
version: None,
output: format!("Using bundled Claude Code sidecar (command creation failed: {})", e),
});
}
};
// Spawn the sidecar and collect output
match sidecar_cmd.spawn() {
Ok((mut rx, _child)) => {
let mut stdout_output = String::new();
let mut stderr_output = String::new();
let mut exit_success = false;
// Collect output from the sidecar
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(data) => {
let line = String::from_utf8_lossy(&data);
stdout_output.push_str(&line);
}
CommandEvent::Stderr(data) => {
let line = String::from_utf8_lossy(&data);
stderr_output.push_str(&line);
}
CommandEvent::Terminated(payload) => {
exit_success = payload.code.unwrap_or(-1) == 0;
break;
}
_ => {}
}
}
// Use regex to directly extract version pattern (e.g., "1.0.41")
let version_regex = regex::Regex::new(r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)").ok();
let version = if let Some(regex) = version_regex {
regex.captures(&stdout_output)
.and_then(|captures| captures.get(1))
.map(|m| m.as_str().to_string())
} else {
None
};
let full_output = if stderr_output.is_empty() {
stdout_output.clone()
} else {
format!("{}\n{}", stdout_output, stderr_output)
};
// Check if the output matches the expected format
let is_valid = stdout_output.contains("(Claude Code)") || stdout_output.contains("Claude Code") || version.is_some();
return Ok(ClaudeVersionStatus {
is_installed: is_valid && exit_success,
version,
output: full_output.trim().to_string(),
});
}
Err(e) => {
log::error!("Failed to execute sidecar: {}", e);
return Ok(ClaudeVersionStatus {
is_installed: true, // We know it exists, just couldn't get version
version: None,
output: format!("Using bundled Claude Code sidecar (version check failed: {})", e),
});
}
}
}
use log::debug;debug!("Claude path: {}", claude_path);
// In production builds, we can't check the version directly
@@ -951,12 +842,8 @@ pub async fn execute_claude_code(
"--dangerously-skip-permissions".to_string(),
];
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
}
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
@@ -987,12 +874,8 @@ pub async fn continue_claude_code(
"--dangerously-skip-permissions".to_string(),
];
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
}
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
@@ -1026,12 +909,8 @@ pub async fn resume_claude_code(
"--dangerously-skip-permissions".to_string(),
];
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
}
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
@@ -1348,144 +1227,6 @@ 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]

View File

@@ -47,19 +47,5 @@
"shell": {
"open": true
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.png"
],
"externalBin": [
"binaries/claude-code"
]
}
}