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.
This commit is contained in:
Catherine
2025-07-11 13:23:33 +08:00
committed by Vivek R
parent 67800087e9
commit 32a197100a
9 changed files with 715 additions and 3 deletions

View File

@@ -452,6 +452,8 @@ fn compare_versions(a: &str, b: &str) -> Ordering {
pub fn create_command_with_env(program: &str) -> Command { pub fn create_command_with_env(program: &str) -> Command {
let mut cmd = Command::new(program); let mut cmd = Command::new(program);
info!("Creating command for: {}", program);
// Inherit essential environment variables from parent process // Inherit essential environment variables from parent process
for (key, value) in std::env::vars() { for (key, value) in std::env::vars() {
// Pass through PATH and other essential environment variables // Pass through PATH and other essential environment variables
@@ -467,12 +469,26 @@ pub fn create_command_with_env(program: &str) -> Command {
|| key == "NVM_BIN" || key == "NVM_BIN"
|| key == "HOMEBREW_PREFIX" || key == "HOMEBREW_PREFIX"
|| key == "HOMEBREW_CELLAR" || 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); debug!("Inheriting env var: {}={}", key, value);
cmd.env(&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 // Add NVM support if the program is in an NVM directory
if program.contains("/.nvm/versions/node/") { if program.contains("/.nvm/versions/node/") {
if let Some(node_bin_dir) = std::path::Path::new(program).parent() { if let Some(node_bin_dir) = std::path::Path::new(program).parent() {

View File

@@ -12,6 +12,7 @@ use std::process::Stdio;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tauri::{AppHandle, Emitter, Manager, State}; use tauri::{AppHandle, Emitter, Manager, State};
use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::ShellExt;
use tauri_plugin_shell::process::CommandEvent;
use tokio::io::{AsyncBufReadExt, BufReader as TokioBufReader}; use tokio::io::{AsyncBufReadExt, BufReader as TokioBufReader};
use tokio::process::Command; use tokio::process::Command;
@@ -766,8 +767,49 @@ pub async fn execute_agent(
"--dangerously-skip-permissions".to_string(), "--dangerously-skip-permissions".to_string(),
]; ];
// Execute using system binary // 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 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);
// 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 /// Creates a system binary command for agent execution
@@ -792,6 +834,186 @@ fn create_agent_system_command(
} }
/// Spawn agent using sidecar 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> {
// 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::<JsonValue>(&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 /// Spawn agent using system binary command
async fn spawn_agent_system( async fn spawn_agent_system(

View File

@@ -266,6 +266,42 @@ fn create_command_with_env(program: &str) -> Command {
tokio_cmd 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);
// 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 /// Creates a system binary command with the given arguments
fn create_system_command( fn create_system_command(

View File

@@ -4,3 +4,4 @@ pub mod mcp;
pub mod usage; pub mod usage;
pub mod storage; pub mod storage;
pub mod slash_commands; pub mod slash_commands;
pub mod proxy;

View File

@@ -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<String>,
pub https_proxy: Option<String>,
pub no_proxy: Option<String>,
pub all_proxy: Option<String>,
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<ProxySettings, String> {
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);
}
}
}

View File

@@ -42,6 +42,7 @@ use commands::storage::{
storage_list_tables, storage_read_table, storage_update_row, storage_delete_row, storage_list_tables, storage_read_table, storage_update_row, storage_delete_row,
storage_insert_row, storage_execute_sql, storage_reset_database, 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 process::ProcessRegistryState;
use std::sync::Mutex; use std::sync::Mutex;
use tauri::Manager; use tauri::Manager;
@@ -57,6 +58,55 @@ fn main() {
.setup(|app| { .setup(|app| {
// Initialize agents database // Initialize agents database
let conn = init_database(&app.handle()).expect("Failed to 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))); app.manage(AgentDb(Mutex::new(conn)));
// Initialize checkpoint state // Initialize checkpoint state
@@ -195,6 +245,10 @@ fn main() {
commands::slash_commands::slash_command_get, commands::slash_commands::slash_command_get,
commands::slash_commands::slash_command_save, commands::slash_commands::slash_command_save,
commands::slash_commands::slash_command_delete, commands::slash_commands::slash_command_delete,
// Proxy Settings
get_proxy_settings,
save_proxy_settings,
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");

View File

@@ -83,6 +83,41 @@ impl ProcessRegistry {
self.register_process_internal(run_id, process_info, child) 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) /// Register a new Claude session (without child process - handled separately)
pub fn register_claude_session( pub fn register_claude_session(
&self, &self,

View File

@@ -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>) => void;
}
export function ProxySettings({ setToast, onChange }: ProxySettingsProps) {
const [settings, setSettings] = useState<ProxySettings>({
http_proxy: null,
https_proxy: null,
no_proxy: null,
all_proxy: null,
enabled: false,
});
const [originalSettings, setOriginalSettings] = useState<ProxySettings>({
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<ProxySettings>('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 (
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium">Proxy Settings</h3>
<p className="text-sm text-muted-foreground">
Configure proxy settings for Claude API requests
</p>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="proxy-enabled">Enable Proxy</Label>
<p className="text-sm text-muted-foreground">
Use proxy for all Claude API requests
</p>
</div>
<Switch
id="proxy-enabled"
checked={settings.enabled}
onCheckedChange={(checked) => setSettings(prev => ({ ...prev, enabled: checked }))}
/>
</div>
<div className="space-y-4" style={{ opacity: settings.enabled ? 1 : 0.5 }}>
<div className="space-y-2">
<Label htmlFor="http-proxy">HTTP Proxy</Label>
<Input
id="http-proxy"
placeholder="http://proxy.example.com:8080"
value={settings.http_proxy || ''}
onChange={(e) => handleInputChange('http_proxy', e.target.value)}
disabled={!settings.enabled}
/>
</div>
<div className="space-y-2">
<Label htmlFor="https-proxy">HTTPS Proxy</Label>
<Input
id="https-proxy"
placeholder="http://proxy.example.com:8080"
value={settings.https_proxy || ''}
onChange={(e) => handleInputChange('https_proxy', e.target.value)}
disabled={!settings.enabled}
/>
</div>
<div className="space-y-2">
<Label htmlFor="no-proxy">No Proxy</Label>
<Input
id="no-proxy"
placeholder="localhost,127.0.0.1,.example.com"
value={settings.no_proxy || ''}
onChange={(e) => handleInputChange('no_proxy', e.target.value)}
disabled={!settings.enabled}
/>
<p className="text-xs text-muted-foreground">
Comma-separated list of hosts that should bypass the proxy
</p>
</div>
<div className="space-y-2">
<Label htmlFor="all-proxy">All Proxy (Optional)</Label>
<Input
id="all-proxy"
placeholder="socks5://proxy.example.com:1080"
value={settings.all_proxy || ''}
onChange={(e) => handleInputChange('all_proxy', e.target.value)}
disabled={!settings.enabled}
/>
<p className="text-xs text-muted-foreground">
Proxy URL to use for all protocols if protocol-specific proxies are not set
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -26,6 +26,7 @@ import { ClaudeVersionSelector } from "./ClaudeVersionSelector";
import { StorageTab } from "./StorageTab"; import { StorageTab } from "./StorageTab";
import { HooksEditor } from "./HooksEditor"; import { HooksEditor } from "./HooksEditor";
import { SlashCommandsManager } from "./SlashCommandsManager"; import { SlashCommandsManager } from "./SlashCommandsManager";
import { ProxySettings } from "./ProxySettings";
import { useTheme } from "@/hooks"; import { useTheme } from "@/hooks";
interface SettingsProps { interface SettingsProps {
@@ -82,6 +83,10 @@ export const Settings: React.FC<SettingsProps> = ({
// Theme hook // Theme hook
const { theme, setTheme, customColors, setCustomColors } = useTheme(); const { theme, setTheme, customColors, setCustomColors } = useTheme();
// Proxy state
const [proxySettingsChanged, setProxySettingsChanged] = useState(false);
const saveProxySettings = React.useRef<(() => Promise<void>) | null>(null);
// Load settings on mount // Load settings on mount
useEffect(() => { useEffect(() => {
loadSettings(); loadSettings();
@@ -198,6 +203,12 @@ export const Settings: React.FC<SettingsProps> = ({
setUserHooksChanged(false); setUserHooksChanged(false);
} }
// Save proxy settings if changed
if (proxySettingsChanged && saveProxySettings.current) {
await saveProxySettings.current();
setProxySettingsChanged(false);
}
setToast({ message: "Settings saved successfully!", type: "success" }); setToast({ message: "Settings saved successfully!", type: "success" });
} catch (err) { } catch (err) {
console.error("Failed to save settings:", err); console.error("Failed to save settings:", err);
@@ -363,7 +374,7 @@ export const Settings: React.FC<SettingsProps> = ({
) : ( ) : (
<div className="flex-1 overflow-y-auto p-4"> <div className="flex-1 overflow-y-auto p-4">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid grid-cols-7 w-full"> <TabsList className="grid grid-cols-8 w-full">
<TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="permissions">Permissions</TabsTrigger> <TabsTrigger value="permissions">Permissions</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger> <TabsTrigger value="environment">Environment</TabsTrigger>
@@ -371,6 +382,7 @@ export const Settings: React.FC<SettingsProps> = ({
<TabsTrigger value="hooks">Hooks</TabsTrigger> <TabsTrigger value="hooks">Hooks</TabsTrigger>
<TabsTrigger value="commands">Commands</TabsTrigger> <TabsTrigger value="commands">Commands</TabsTrigger>
<TabsTrigger value="storage">Storage</TabsTrigger> <TabsTrigger value="storage">Storage</TabsTrigger>
<TabsTrigger value="proxy">Proxy</TabsTrigger>
</TabsList> </TabsList>
{/* General Settings */} {/* General Settings */}
@@ -872,6 +884,19 @@ export const Settings: React.FC<SettingsProps> = ({
<TabsContent value="storage"> <TabsContent value="storage">
<StorageTab /> <StorageTab />
</TabsContent> </TabsContent>
{/* Proxy Settings */}
<TabsContent value="proxy">
<Card className="p-6">
<ProxySettings
setToast={setToast}
onChange={(hasChanges, _getSettings, save) => {
setProxySettingsChanged(hasChanges);
saveProxySettings.current = save;
}}
/>
</Card>
</TabsContent>
</Tabs> </Tabs>
</div> </div>
)} )}