From d6c1b0f98ae9697d2ff6eb774cb074d9c74a4aa7 Mon Sep 17 00:00:00 2001 From: Catherine Date: Fri, 11 Jul 2025 13:23:33 +0800 Subject: [PATCH 1/4] feat: add proxy configuration support - Add proxy settings UI component with enable/disable toggle - Support HTTP, HTTPS, NO_PROXY, and ALL_PROXY environment variables - Store proxy settings in app database for persistence - Apply proxy settings on app startup and when saved - Pass proxy environment variables to Claude command execution - Integrate proxy settings into main Settings page with unified save - Add proxy support for both system binary and sidecar execution This allows users to configure proxy settings for Claude API requests, which is essential for users behind corporate firewalls or in regions requiring proxy access. Fixes network connectivity issues in restricted environments. --- src-tauri/src/claude_binary.rs | 16 +++ src-tauri/src/commands/agents.rs | 226 +++++++++++++++++++++++++++++- src-tauri/src/commands/claude.rs | 36 +++++ src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/proxy.rs | 155 ++++++++++++++++++++ src-tauri/src/main.rs | 54 +++++++ src-tauri/src/process/registry.rs | 35 +++++ src/components/ProxySettings.tsx | 168 ++++++++++++++++++++++ src/components/Settings.tsx | 27 +++- 9 files changed, 715 insertions(+), 3 deletions(-) create mode 100644 src-tauri/src/commands/proxy.rs create mode 100644 src/components/ProxySettings.tsx diff --git a/src-tauri/src/claude_binary.rs b/src-tauri/src/claude_binary.rs index 7dc8b98af..272a1cc92 100644 --- a/src-tauri/src/claude_binary.rs +++ b/src-tauri/src/claude_binary.rs @@ -451,6 +451,8 @@ fn compare_versions(a: &str, b: &str) -> Ordering { /// This ensures commands like Claude can find Node.js and other dependencies pub fn create_command_with_env(program: &str) -> Command { let mut cmd = Command::new(program); + + info!("Creating command for: {}", program); // Inherit essential environment variables from parent process for (key, value) in std::env::vars() { @@ -467,11 +469,25 @@ pub fn create_command_with_env(program: &str) -> Command { || key == "NVM_BIN" || key == "HOMEBREW_PREFIX" || key == "HOMEBREW_CELLAR" + // Add proxy environment variables (only uppercase) + || key == "HTTP_PROXY" + || key == "HTTPS_PROXY" + || key == "NO_PROXY" + || key == "ALL_PROXY" { debug!("Inheriting env var: {}={}", key, value); cmd.env(&key, &value); } } + + // Log proxy-related environment variables for debugging + info!("Command will use proxy settings:"); + if let Ok(http_proxy) = std::env::var("HTTP_PROXY") { + info!(" HTTP_PROXY={}", http_proxy); + } + if let Ok(https_proxy) = std::env::var("HTTPS_PROXY") { + info!(" HTTPS_PROXY={}", https_proxy); + } // Add NVM support if the program is in an NVM directory if program.contains("/.nvm/versions/node/") { diff --git a/src-tauri/src/commands/agents.rs b/src-tauri/src/commands/agents.rs index 349e2bfeb..8a5c1e50a 100644 --- a/src-tauri/src/commands/agents.rs +++ b/src-tauri/src/commands/agents.rs @@ -12,6 +12,7 @@ use std::process::Stdio; use std::sync::{Arc, Mutex}; use tauri::{AppHandle, Emitter, Manager, State}; use tauri_plugin_shell::ShellExt; +use tauri_plugin_shell::process::CommandEvent; use tokio::io::{AsyncBufReadExt, BufReader as TokioBufReader}; use tokio::process::Command; @@ -766,8 +767,49 @@ pub async fn execute_agent( "--dangerously-skip-permissions".to_string(), ]; - // 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 + // 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, + project_path: &str, +) -> Result { + 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); + + // Pass through proxy environment variables if they exist (only uppercase) + for (key, value) in std::env::vars() { + if key == "HTTP_PROXY" + || key == "HTTPS_PROXY" + || key == "NO_PROXY" + || key == "ALL_PROXY" + { + debug!("Setting proxy env var for agent sidecar: {}={}", key, value); + sidecar_cmd = sidecar_cmd.env(&key, &value); + } + } + + Ok(sidecar_cmd) } /// Creates a system binary command for agent execution @@ -792,6 +834,186 @@ 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, + project_path: String, + task: String, + execution_model: String, + db: State<'_, AgentDb>, + registry: State<'_, crate::process::ProcessRegistryState>, +) -> Result { + // Build the sidecar command + let sidecar_cmd = create_agent_sidecar_command(&app, args, &project_path)?; + + // Spawn the process + info!("🚀 Spawning Claude sidecar process..."); + let (mut child, mut receiver) = sidecar_cmd.spawn().map_err(|e| { + error!("❌ Failed to spawn Claude sidecar process: {}", e); + format!("Failed to spawn Claude sidecar: {}", e) + })?; + + // Get the PID + let pid = child.pid() as u32; + let now = chrono::Utc::now().to_rfc3339(); + info!("✅ Claude sidecar process spawned successfully with PID: {}", pid); + + // Update the database with PID and status + { + 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"); + } + + // Get app directory for database path + let app_dir = app + .path() + .app_data_dir() + .expect("Failed to get app data dir"); + let db_path = app_dir.join("agents.db"); + + // Shared state for collecting session ID and live output + let session_id = std::sync::Arc::new(Mutex::new(String::new())); + let live_output = std::sync::Arc::new(Mutex::new(String::new())); + let start_time = std::time::Instant::now(); + + // Register the process in the registry + registry + .0 + .register_sidecar_process( + run_id, + agent_id, + agent_name, + pid, + project_path.clone(), + task.clone(), + execution_model.clone(), + child, + ) + .map_err(|e| format!("Failed to register sidecar process: {}", e))?; + info!("📋 Registered sidecar process in registry"); + + // Handle sidecar events + let app_handle = app.clone(); + let session_id_clone = session_id.clone(); + 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 db_path_for_sidecar = db_path.clone(); + + tokio::spawn(async move { + info!("📖 Starting to read Claude sidecar events..."); + let mut line_count = 0; + + while let Some(event) = receiver.recv().await { + match event { + CommandEvent::Stdout(line) => { + line_count += 1; + + // Log first output + if !first_output_clone.load(std::sync::atomic::Ordering::Relaxed) { + info!( + "🎉 First output received from Claude sidecar process! 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 + let _ = registry_clone.append_live_output(run_id, &line); + + // Extract session ID from JSONL output + if let Ok(json) = serde_json::from_str::(&line) { + 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_clone.lock() { + if current_session_id.is_empty() { + *current_session_id = sid.to_string(); + info!("🔑 Extracted session ID: {}", sid); + + // Update database immediately with session ID + if let Ok(conn) = Connection::open(&db_path_for_sidecar) { + 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 + let _ = app_handle.emit(&format!("agent-output:{}", run_id), &line); + let _ = app_handle.emit("agent-output", &line); + } + CommandEvent::Stderr(line) => { + error!("sidecar stderr: {}", line); + let _ = app_handle.emit(&format!("agent-error:{}", run_id), &line); + let _ = app_handle.emit("agent-error", &line); + } + CommandEvent::Terminated(payload) => { + info!("Claude sidecar process terminated with code: {:?}", payload.code); + + // Get the session ID + let extracted_session_id = if let Ok(sid) = session_id.lock() { + sid.clone() + } else { + String::new() + }; + + // Update database with completion + if let Ok(conn) = Connection::open(&db_path) { + let _ = conn.execute( + "UPDATE agent_runs SET session_id = ?1, status = 'completed', completed_at = CURRENT_TIMESTAMP WHERE id = ?2", + params![extracted_session_id, run_id], + ); + } + + let success = payload.code.unwrap_or(1) == 0; + let _ = app.emit("agent-complete", success); + let _ = app.emit(&format!("agent-complete:{}", run_id), success); + break; + } + _ => {} + } + } + + info!("📖 Finished reading Claude sidecar events. Total lines: {}", line_count); + }); + + Ok(run_id) +} /// Spawn agent using system binary command async fn spawn_agent_system( diff --git a/src-tauri/src/commands/claude.rs b/src-tauri/src/commands/claude.rs index 8b964803f..222ee6f62 100644 --- a/src-tauri/src/commands/claude.rs +++ b/src-tauri/src/commands/claude.rs @@ -266,6 +266,42 @@ 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, + project_path: &str, +) -> Result { + 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); + + // Pass through proxy environment variables if they exist (only uppercase) + for (key, value) in std::env::vars() { + if key == "HTTP_PROXY" + || key == "HTTPS_PROXY" + || key == "NO_PROXY" + || key == "ALL_PROXY" + { + log::debug!("Setting proxy env var for sidecar: {}={}", key, value); + sidecar_cmd = sidecar_cmd.env(&key, &value); + } + } + + Ok(sidecar_cmd) +} /// Creates a system binary command with the given arguments fn create_system_command( diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index dec0c08cf..a0fa7e897 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -4,3 +4,4 @@ pub mod mcp; pub mod usage; pub mod storage; pub mod slash_commands; +pub mod proxy; diff --git a/src-tauri/src/commands/proxy.rs b/src-tauri/src/commands/proxy.rs new file mode 100644 index 000000000..e2454ecfe --- /dev/null +++ b/src-tauri/src/commands/proxy.rs @@ -0,0 +1,155 @@ +use serde::{Deserialize, Serialize}; +use tauri::State; +use rusqlite::params; + +use crate::commands::agents::AgentDb; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ProxySettings { + pub http_proxy: Option, + pub https_proxy: Option, + pub no_proxy: Option, + pub all_proxy: Option, + pub enabled: bool, +} + +impl Default for ProxySettings { + fn default() -> Self { + Self { + http_proxy: None, + https_proxy: None, + no_proxy: None, + all_proxy: None, + enabled: false, + } + } +} + +/// Get proxy settings from the database +#[tauri::command] +pub async fn get_proxy_settings(db: State<'_, AgentDb>) -> Result { + let conn = db.0.lock().map_err(|e| e.to_string())?; + + let mut settings = ProxySettings::default(); + + // Query each proxy setting + let keys = vec![ + ("proxy_enabled", "enabled"), + ("proxy_http", "http_proxy"), + ("proxy_https", "https_proxy"), + ("proxy_no", "no_proxy"), + ("proxy_all", "all_proxy"), + ]; + + for (db_key, field) in keys { + if let Ok(value) = conn.query_row( + "SELECT value FROM app_settings WHERE key = ?1", + params![db_key], + |row| row.get::<_, String>(0), + ) { + match field { + "enabled" => settings.enabled = value == "true", + "http_proxy" => settings.http_proxy = Some(value).filter(|s| !s.is_empty()), + "https_proxy" => settings.https_proxy = Some(value).filter(|s| !s.is_empty()), + "no_proxy" => settings.no_proxy = Some(value).filter(|s| !s.is_empty()), + "all_proxy" => settings.all_proxy = Some(value).filter(|s| !s.is_empty()), + _ => {} + } + } + } + + Ok(settings) +} + +/// Save proxy settings to the database +#[tauri::command] +pub async fn save_proxy_settings( + db: State<'_, AgentDb>, + settings: ProxySettings, +) -> Result<(), String> { + let conn = db.0.lock().map_err(|e| e.to_string())?; + + // Save each setting + let values = vec![ + ("proxy_enabled", settings.enabled.to_string()), + ("proxy_http", settings.http_proxy.clone().unwrap_or_default()), + ("proxy_https", settings.https_proxy.clone().unwrap_or_default()), + ("proxy_no", settings.no_proxy.clone().unwrap_or_default()), + ("proxy_all", settings.all_proxy.clone().unwrap_or_default()), + ]; + + for (key, value) in values { + conn.execute( + "INSERT OR REPLACE INTO app_settings (key, value) VALUES (?1, ?2)", + params![key, value], + ).map_err(|e| format!("Failed to save {}: {}", key, e))?; + } + + // Apply the proxy settings immediately to the current process + apply_proxy_settings(&settings); + + Ok(()) +} + +/// Apply proxy settings as environment variables +pub fn apply_proxy_settings(settings: &ProxySettings) { + log::info!("Applying proxy settings: enabled={}", settings.enabled); + + if !settings.enabled { + // Clear proxy environment variables if disabled + log::info!("Clearing proxy environment variables"); + std::env::remove_var("HTTP_PROXY"); + std::env::remove_var("HTTPS_PROXY"); + std::env::remove_var("NO_PROXY"); + std::env::remove_var("ALL_PROXY"); + // Also clear lowercase versions + std::env::remove_var("http_proxy"); + std::env::remove_var("https_proxy"); + std::env::remove_var("no_proxy"); + std::env::remove_var("all_proxy"); + return; + } + + // Ensure NO_PROXY includes localhost by default + let mut no_proxy_list = vec!["localhost", "127.0.0.1", "::1", "0.0.0.0"]; + if let Some(user_no_proxy) = &settings.no_proxy { + if !user_no_proxy.is_empty() { + no_proxy_list.push(user_no_proxy.as_str()); + } + } + let no_proxy_value = no_proxy_list.join(","); + + // Set proxy environment variables (uppercase is standard) + if let Some(http_proxy) = &settings.http_proxy { + if !http_proxy.is_empty() { + log::info!("Setting HTTP_PROXY={}", http_proxy); + std::env::set_var("HTTP_PROXY", http_proxy); + } + } + + if let Some(https_proxy) = &settings.https_proxy { + if !https_proxy.is_empty() { + log::info!("Setting HTTPS_PROXY={}", https_proxy); + std::env::set_var("HTTPS_PROXY", https_proxy); + } + } + + // Always set NO_PROXY to include localhost + log::info!("Setting NO_PROXY={}", no_proxy_value); + std::env::set_var("NO_PROXY", &no_proxy_value); + + if let Some(all_proxy) = &settings.all_proxy { + if !all_proxy.is_empty() { + log::info!("Setting ALL_PROXY={}", all_proxy); + std::env::set_var("ALL_PROXY", all_proxy); + } + } + + // Log current proxy environment variables for debugging + log::info!("Current proxy environment variables:"); + for (key, value) in std::env::vars() { + if key.contains("PROXY") || key.contains("proxy") { + log::info!(" {}={}", key, value); + } + } +} \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 1ee996026..0589bef75 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -42,6 +42,7 @@ use commands::storage::{ storage_list_tables, storage_read_table, storage_update_row, storage_delete_row, storage_insert_row, storage_execute_sql, storage_reset_database, }; +use commands::proxy::{get_proxy_settings, save_proxy_settings, apply_proxy_settings}; use process::ProcessRegistryState; use std::sync::Mutex; use tauri::Manager; @@ -57,6 +58,55 @@ fn main() { .setup(|app| { // Initialize agents database let conn = init_database(&app.handle()).expect("Failed to initialize agents database"); + + // Load and apply proxy settings from the database + { + let db = AgentDb(Mutex::new(conn)); + let proxy_settings = match db.0.lock() { + Ok(conn) => { + // Directly query proxy settings from the database + let mut settings = commands::proxy::ProxySettings::default(); + + let keys = vec![ + ("proxy_enabled", "enabled"), + ("proxy_http", "http_proxy"), + ("proxy_https", "https_proxy"), + ("proxy_no", "no_proxy"), + ("proxy_all", "all_proxy"), + ]; + + for (db_key, field) in keys { + if let Ok(value) = conn.query_row( + "SELECT value FROM app_settings WHERE key = ?1", + rusqlite::params![db_key], + |row| row.get::<_, String>(0), + ) { + match field { + "enabled" => settings.enabled = value == "true", + "http_proxy" => settings.http_proxy = Some(value).filter(|s| !s.is_empty()), + "https_proxy" => settings.https_proxy = Some(value).filter(|s| !s.is_empty()), + "no_proxy" => settings.no_proxy = Some(value).filter(|s| !s.is_empty()), + "all_proxy" => settings.all_proxy = Some(value).filter(|s| !s.is_empty()), + _ => {} + } + } + } + + log::info!("Loaded proxy settings: enabled={}", settings.enabled); + settings + } + Err(e) => { + log::warn!("Failed to lock database for proxy settings: {}", e); + commands::proxy::ProxySettings::default() + } + }; + + // Apply the proxy settings + apply_proxy_settings(&proxy_settings); + } + + // Re-open the connection for the app to manage + let conn = init_database(&app.handle()).expect("Failed to initialize agents database"); app.manage(AgentDb(Mutex::new(conn))); // Initialize checkpoint state @@ -195,6 +245,10 @@ fn main() { commands::slash_commands::slash_command_get, commands::slash_commands::slash_command_save, commands::slash_commands::slash_command_delete, + + // Proxy Settings + get_proxy_settings, + save_proxy_settings, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/process/registry.rs b/src-tauri/src/process/registry.rs index 6423b900e..8432e6bd0 100644 --- a/src-tauri/src/process/registry.rs +++ b/src-tauri/src/process/registry.rs @@ -83,6 +83,41 @@ impl ProcessRegistry { self.register_process_internal(run_id, process_info, child) } + /// Register a new running agent process using sidecar (similar to register_process but for sidecar children) + pub fn register_sidecar_process( + &self, + run_id: i64, + agent_id: i64, + agent_name: String, + pid: u32, + project_path: String, + task: String, + model: String, + child: tauri_plugin_shell::process::Child, + ) -> Result<(), String> { + let process_info = ProcessInfo { + run_id, + process_type: ProcessType::AgentRun { agent_id, agent_name }, + pid, + started_at: Utc::now(), + project_path, + task, + model, + }; + + // For sidecar processes, we register without the child handle since it's managed differently + let mut processes = self.processes.lock().map_err(|e| e.to_string())?; + + let process_handle = ProcessHandle { + info: process_info, + child: Arc::new(Mutex::new(None)), // No tokio::process::Child handle for sidecar + live_output: Arc::new(Mutex::new(String::new())), + }; + + processes.insert(run_id, process_handle); + Ok(()) + } + /// Register a new Claude session (without child process - handled separately) pub fn register_claude_session( &self, diff --git a/src/components/ProxySettings.tsx b/src/components/ProxySettings.tsx new file mode 100644 index 000000000..0b8d6e288 --- /dev/null +++ b/src/components/ProxySettings.tsx @@ -0,0 +1,168 @@ +import { useState, useEffect } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; + +export interface ProxySettings { + http_proxy: string | null; + https_proxy: string | null; + no_proxy: string | null; + all_proxy: string | null; + enabled: boolean; +} + +interface ProxySettingsProps { + setToast: (toast: { message: string; type: 'success' | 'error' } | null) => void; + onChange?: (hasChanges: boolean, getSettings: () => ProxySettings, saveSettings: () => Promise) => void; +} + +export function ProxySettings({ setToast, onChange }: ProxySettingsProps) { + const [settings, setSettings] = useState({ + http_proxy: null, + https_proxy: null, + no_proxy: null, + all_proxy: null, + enabled: false, + }); + const [originalSettings, setOriginalSettings] = useState({ + http_proxy: null, + https_proxy: null, + no_proxy: null, + all_proxy: null, + enabled: false, + }); + + useEffect(() => { + loadSettings(); + }, []); + + // Save settings function + const saveSettings = async () => { + try { + await invoke('save_proxy_settings', { settings }); + setOriginalSettings(settings); + setToast({ + message: 'Proxy settings saved and applied successfully.', + type: 'success', + }); + } catch (error) { + console.error('Failed to save proxy settings:', error); + setToast({ + message: 'Failed to save proxy settings', + type: 'error', + }); + throw error; // Re-throw to let parent handle the error + } + }; + + // Notify parent component of changes + useEffect(() => { + if (onChange) { + const hasChanges = JSON.stringify(settings) !== JSON.stringify(originalSettings); + onChange(hasChanges, () => settings, saveSettings); + } + }, [settings, originalSettings, onChange]); + + const loadSettings = async () => { + try { + const loadedSettings = await invoke('get_proxy_settings'); + setSettings(loadedSettings); + setOriginalSettings(loadedSettings); + } catch (error) { + console.error('Failed to load proxy settings:', error); + setToast({ + message: 'Failed to load proxy settings', + type: 'error', + }); + } + }; + + + const handleInputChange = (field: keyof ProxySettings, value: string) => { + setSettings(prev => ({ + ...prev, + [field]: value || null, + })); + }; + + return ( +
+
+

Proxy Settings

+

+ Configure proxy settings for Claude API requests +

+
+ +
+
+
+ +

+ Use proxy for all Claude API requests +

+
+ setSettings(prev => ({ ...prev, enabled: checked }))} + /> +
+ +
+
+ + handleInputChange('http_proxy', e.target.value)} + disabled={!settings.enabled} + /> +
+ +
+ + handleInputChange('https_proxy', e.target.value)} + disabled={!settings.enabled} + /> +
+ +
+ + handleInputChange('no_proxy', e.target.value)} + disabled={!settings.enabled} + /> +

+ Comma-separated list of hosts that should bypass the proxy +

+
+ +
+ + handleInputChange('all_proxy', e.target.value)} + disabled={!settings.enabled} + /> +

+ Proxy URL to use for all protocols if protocol-specific proxies are not set +

+
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 79fa7912b..9650dfc74 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -26,6 +26,7 @@ import { ClaudeVersionSelector } from "./ClaudeVersionSelector"; import { StorageTab } from "./StorageTab"; import { HooksEditor } from "./HooksEditor"; import { SlashCommandsManager } from "./SlashCommandsManager"; +import { ProxySettings } from "./ProxySettings"; import { useTheme } from "@/hooks"; interface SettingsProps { @@ -82,6 +83,10 @@ export const Settings: React.FC = ({ // Theme hook const { theme, setTheme, customColors, setCustomColors } = useTheme(); + // Proxy state + const [proxySettingsChanged, setProxySettingsChanged] = useState(false); + const saveProxySettings = React.useRef<(() => Promise) | null>(null); + // Load settings on mount useEffect(() => { loadSettings(); @@ -198,6 +203,12 @@ export const Settings: React.FC = ({ setUserHooksChanged(false); } + // Save proxy settings if changed + if (proxySettingsChanged && saveProxySettings.current) { + await saveProxySettings.current(); + setProxySettingsChanged(false); + } + setToast({ message: "Settings saved successfully!", type: "success" }); } catch (err) { console.error("Failed to save settings:", err); @@ -363,7 +374,7 @@ export const Settings: React.FC = ({ ) : (
- + General Permissions Environment @@ -371,6 +382,7 @@ export const Settings: React.FC = ({ Hooks Commands Storage + Proxy {/* General Settings */} @@ -872,6 +884,19 @@ export const Settings: React.FC = ({ + + {/* Proxy Settings */} + + + { + setProxySettingsChanged(hasChanges); + saveProxySettings.current = save; + }} + /> + +
)} From b2e7c57f32ceda63cf4d0c1fb91d95840a3f7b25 Mon Sep 17 00:00:00 2001 From: Vivek R <123vivekr@gmail.com> Date: Mon, 28 Jul 2025 20:52:42 +0530 Subject: [PATCH 2/4] refactor(agents): clean up agent command implementation - Remove unused imports (regex, Arc) - Fix receiver/child tuple order from spawn() call - Convert CommandEvent bytes to strings properly - Remove unused variables (_start_time, mut from installations) - Update function signatures to match new API - Simplify child process registration by removing child parameter --- src-tauri/src/commands/agents.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src-tauri/src/commands/agents.rs b/src-tauri/src/commands/agents.rs index 8a5c1e50a..43d8346b7 100644 --- a/src-tauri/src/commands/agents.rs +++ b/src-tauri/src/commands/agents.rs @@ -2,14 +2,13 @@ use anyhow::Result; use chrono; use dirs; use log::{debug, error, info, warn}; -use regex; use reqwest; use rusqlite::{params, Connection, Result as SqliteResult}; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; use std::io::{BufRead, BufReader}; use std::process::Stdio; -use std::sync::{Arc, Mutex}; +use std::sync::Mutex; use tauri::{AppHandle, Emitter, Manager, State}; use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::process::CommandEvent; @@ -851,13 +850,13 @@ async fn spawn_agent_sidecar( // Spawn the process info!("🚀 Spawning Claude sidecar process..."); - let (mut child, mut receiver) = sidecar_cmd.spawn().map_err(|e| { + let (mut receiver, child) = sidecar_cmd.spawn().map_err(|e| { error!("❌ Failed to spawn Claude sidecar process: {}", e); format!("Failed to spawn Claude sidecar: {}", e) })?; - // Get the PID - let pid = child.pid() as u32; + // Get the PID from child + let pid = child.pid(); let now = chrono::Utc::now().to_rfc3339(); info!("✅ Claude sidecar process spawned successfully with PID: {}", pid); @@ -881,7 +880,7 @@ async fn spawn_agent_sidecar( // Shared state for collecting session ID and live output let session_id = std::sync::Arc::new(Mutex::new(String::new())); let live_output = std::sync::Arc::new(Mutex::new(String::new())); - let start_time = std::time::Instant::now(); + let _start_time = std::time::Instant::now(); // Register the process in the registry registry @@ -890,11 +889,10 @@ async fn spawn_agent_sidecar( run_id, agent_id, agent_name, - pid, + pid as u32, project_path.clone(), task.clone(), execution_model.clone(), - child, ) .map_err(|e| format!("Failed to register sidecar process: {}", e))?; info!("📋 Registered sidecar process in registry"); @@ -914,7 +912,8 @@ async fn spawn_agent_sidecar( while let Some(event) = receiver.recv().await { match event { - CommandEvent::Stdout(line) => { + CommandEvent::Stdout(line_bytes) => { + let line = String::from_utf8_lossy(&line_bytes); line_count += 1; // Log first output @@ -977,7 +976,8 @@ async fn spawn_agent_sidecar( let _ = app_handle.emit(&format!("agent-output:{}", run_id), &line); let _ = app_handle.emit("agent-output", &line); } - CommandEvent::Stderr(line) => { + CommandEvent::Stderr(line_bytes) => { + let line = String::from_utf8_lossy(&line_bytes); error!("sidecar stderr: {}", line); let _ = app_handle.emit(&format!("agent-error:{}", run_id), &line); let _ = app_handle.emit("agent-error", &line); @@ -1822,9 +1822,9 @@ pub async fn set_claude_binary_path(db: State<'_, AgentDb>, path: String) -> Res /// List all available Claude installations on the system #[tauri::command] pub async fn list_claude_installations( - app: AppHandle, + _app: AppHandle, ) -> Result, String> { - let mut installations = crate::claude_binary::discover_claude_installations(); + let installations = crate::claude_binary::discover_claude_installations(); if installations.is_empty() { return Err("No Claude Code installations found on the system".to_string()); From 5b0e6d1b435750d4c8f76ef5fea175924c23531e Mon Sep 17 00:00:00 2001 From: Vivek R <123vivekr@gmail.com> Date: Mon, 28 Jul 2025 20:52:57 +0530 Subject: [PATCH 3/4] chore(claude): remove unused sidecar execution code - Remove unused imports (tauri_plugin_shell modules) - Delete should_use_sidecar() function - Delete create_sidecar_command() function - Clean up dead code related to sidecar process execution --- src-tauri/src/commands/claude.rs | 39 -------------------------------- 1 file changed, 39 deletions(-) diff --git a/src-tauri/src/commands/claude.rs b/src-tauri/src/commands/claude.rs index 222ee6f62..8a6a76c55 100644 --- a/src-tauri/src/commands/claude.rs +++ b/src-tauri/src/commands/claude.rs @@ -9,8 +9,6 @@ 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; use regex; /// Global state to track current Claude process @@ -266,43 +264,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, - project_path: &str, -) -> Result { - 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); - - // Pass through proxy environment variables if they exist (only uppercase) - for (key, value) in std::env::vars() { - if key == "HTTP_PROXY" - || key == "HTTPS_PROXY" - || key == "NO_PROXY" - || key == "ALL_PROXY" - { - log::debug!("Setting proxy env var for sidecar: {}={}", key, value); - sidecar_cmd = sidecar_cmd.env(&key, &value); - } - } - - Ok(sidecar_cmd) -} - /// Creates a system binary command with the given arguments fn create_system_command( claude_path: &str, From 75881db9994164b31eb60c48b2d7633183767c6a Mon Sep 17 00:00:00 2001 From: Vivek R <123vivekr@gmail.com> Date: Mon, 28 Jul 2025 20:53:09 +0530 Subject: [PATCH 4/4] refactor(registry): simplify process registration - Remove unused child parameter from register_agent_process() - Simplify ProcessInfo structure by removing unnecessary process handle --- src-tauri/src/process/registry.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src-tauri/src/process/registry.rs b/src-tauri/src/process/registry.rs index 8432e6bd0..30c8e94d3 100644 --- a/src-tauri/src/process/registry.rs +++ b/src-tauri/src/process/registry.rs @@ -93,7 +93,6 @@ impl ProcessRegistry { project_path: String, task: String, model: String, - child: tauri_plugin_shell::process::Child, ) -> Result<(), String> { let process_info = ProcessInfo { run_id,