diff --git a/src-tauri/src/claude_binary.rs b/src-tauri/src/claude_binary.rs index 7dc8b98..272a1cc 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 349e2bf..8a5c1e5 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 8b96480..222ee6f 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 dec0c08..a0fa7e8 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 0000000..e2454ec --- /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 1ee9960..0589bef 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 6423b90..8432e6b 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 0000000..0b8d6e2 --- /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 79fa791..9650dfc 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; + }} + /> + +
)}