feat(settings): HTTP Proxy settings for the IDE's outgoing connections

Add Appearance & Behavior › System Settings › HTTP Proxy with three modes:
No proxy, Auto-detect proxy settings (the default; environment proxy as
before) and Manual proxy configuration (HTTP or SOCKS, host and port, a
comma-separated "No proxy for" list with `*` wildcards, optional proxy
authentication). "Check connection…" GETs a URL with the settings as edited
and reports the status or the error.

- ide-core: `ProxySettings` plus routing (no-proxy globs, loopback always
  direct, credentials percent-encoded), the current proxy for the process,
  curl arguments and child-process variables.
- ide-http: `client::build` / `client::shared` is the one HTTP client
  constructor; the HTTP Client requests and the update check go through it.
- ide-tools: curl downloads (tools, JDKs, debug adapters, agent registry,
  remote server) follow the setting.
- A manual proxy reaches child processes (language servers, debug adapters,
  terminals, git) as HTTP_PROXY / HTTPS_PROXY / ALL_PROXY / NO_PROXY.
- The proxy password is kept in the owner-only saved passwords file
  (`proxy-password.json` in the app data directory), not in settings.toml.

Co-Authored-By: Claude <[email protected]>
Цей коміт міститься в:
l0ng-aiтаClaude committed 2026-10-29 10:19:11 +08:00
1 джерело 75e7d582d4
коміт c1dece1164
27 files changed
+1339 -65

No files matched your search

+3 -5
Переглянути файл
@@ -128,7 +128,7 @@ pub fn settings_get(window: Window, state: State<'_, AppState>) -> Settings {
#[tauri::command]
#[specta::specta]
pub fn settings_get_global(state: State<'_, AppState>) -> Settings {
state.global_settings.effective()
state.host.with_proxy_password(state.global_settings.effective())
}
/// `value` is JSON text, e.g. `14` or `"dark"`. Every window (for a project
@@ -196,11 +196,9 @@ pub async fn settings_import(window: Window, state: State<'_, AppState>, path: P
tauri::async_runtime::spawn(async move { lsp.reconfigure().await });
}
crate::commands::ssh::apply_ssh_settings(&state);
state.host.apply_proxy();
state.sink.emit(ide_core::IdeEvent::SettingsChanged { key: String::new() });
let settings = match state.project(window.label()) {
Ok(p) => p.settings.effective(),
Err(_) => state.global_settings.effective(),
};
let settings = ide_host::ops::project::settings(&state.host, window.label());
Ok(SettingsImport { settings, storage })
}
+8
Переглянути файл
@@ -76,3 +76,11 @@ pub fn http_history_get(window: Window, state: State<'_, AppState>, id: String)
pub fn http_history_clear(window: Window, state: State<'_, AppState>) -> IdeResult<()> {
state.project(window.label())?.http_history.clear()
}
/// System Settings › HTTP Proxy › Check connection: GETs `url` through `proxy` as
/// edited (not yet applied); the response status, or why there was none.
#[tauri::command]
#[specta::specta]
pub async fn proxy_check_connection(proxy: ide_core::settings::ProxySettings, url: String) -> IdeResult<String> {
ide_http::client::check_connection(&proxy, &url, &ide_core::CancellationToken::new()).await
}
+1
Переглянути файл
@@ -394,6 +394,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
http::http_history,
http::http_history_get,
http::http_history_clear,
http::proxy_check_connection,
db::db_sources,
db::db_save_sources,
db::db_set_password,
+2 -1
Переглянути файл
@@ -295,7 +295,8 @@ fn updater(app: &AppHandle) -> IdeResult<Option<tauri_plugin_updater::Updater>>
return Ok(None);
}
let env = update_endpoint_env();
let mut builder = app.updater_builder();
let proxy = ide_core::proxy_settings::proxy();
let mut builder = app.updater_builder().configure_client(move |b| ide_http::client::with_proxy(b, &proxy));
if let Some(url) = env {
let url = url.parse().map_err(|e| IdeError::invalid(format!("OPENIDEA_UPDATE_ENDPOINT: {e}")))?;
builder = builder.endpoints(vec![url]).map_err(|e| IdeError::invalid(e.to_string()))?;