дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(settings): HTTP Proxy settings for the IDE's outgoing connections (#194)
* 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]> * fix(host): Allow the Unix-only post-start test helpers on Windows Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
Цей коміт міститься в:
1 джерело
c8e3fe9d2e
коміт
9d4e778c7b
28 files changed
+1343
-65
No files matched your search
@@ -18,6 +18,7 @@ import {
|
||||
import { EDITOR_EXTRA_DEFAULTS, validateEditorExtras, type EditorExtras } from "@/platform/editorSettings";
|
||||
import { DEFAULT_WORKFLOW, validateWorkflow, type WorkflowSettings } from "@/platform/workflowSettings";
|
||||
import { DEFAULT_INSPECTIONS, validateInspections, type InspectionSettingsValid } from "@/platform/inspections";
|
||||
import { DEFAULT_PROXY, validateProxy, type ProxySettingsValid } from "@/platform/proxySettings";
|
||||
import {
|
||||
DEFAULT_APPEARANCE_EXTRAS,
|
||||
DEFAULT_GENERAL_EXTRAS,
|
||||
@@ -77,6 +78,8 @@ export interface ValidSettings extends WorkflowSettings {
|
||||
keymapOverrides: Record<string, string[]>;
|
||||
customKeymaps: Record<string, CustomKeymapSettings>;
|
||||
notifications: NotificationsValid;
|
||||
/** System Settings › HTTP Proxy (global). */
|
||||
proxy: ProxySettingsValid;
|
||||
languages: LanguagesValid;
|
||||
projectStructure: ProjectStructureValid;
|
||||
inspections: InspectionSettingsValid;
|
||||
@@ -143,6 +146,7 @@ export const DEFAULT_SETTINGS: ValidSettings = {
|
||||
keymapOverrides: {},
|
||||
customKeymaps: {},
|
||||
notifications: DEFAULT_NOTIFICATIONS,
|
||||
proxy: DEFAULT_PROXY,
|
||||
languages: DEFAULT_LANGUAGES,
|
||||
projectStructure: DEFAULT_PROJECT_STRUCTURE,
|
||||
inspections: DEFAULT_INSPECTIONS,
|
||||
@@ -245,6 +249,7 @@ export function validateSettings(raw: Settings | unknown): ValidSettings {
|
||||
keymapOverrides: overrides,
|
||||
customKeymaps,
|
||||
notifications: validateNotifications(root.notifications),
|
||||
proxy: validateProxy(root.proxy),
|
||||
languages: validateLanguages(root.languages),
|
||||
projectStructure: validateProjectStructure(root.projectStructure),
|
||||
inspections: validateInspections(root.inspections),
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.modes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.radio {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 28px;
|
||||
}
|
||||
.radio input {
|
||||
margin: 0;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
.disabled {
|
||||
color: var(--fg-subtle);
|
||||
}
|
||||
.hint {
|
||||
padding-left: 20px;
|
||||
color: var(--fg-subtle);
|
||||
}
|
||||
|
||||
/* Manual configuration, indented under its radio button. */
|
||||
.manual {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.kinds {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.hostRow {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
}
|
||||
.host {
|
||||
width: 220px;
|
||||
}
|
||||
.port {
|
||||
width: 64px;
|
||||
}
|
||||
.wide {
|
||||
width: 360px;
|
||||
}
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
}
|
||||
.auth {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.error {
|
||||
color: var(--danger);
|
||||
}
|
||||
.ok {
|
||||
color: var(--success, var(--fg-muted));
|
||||
}
|
||||
|
||||
.check {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
max-width: 480px;
|
||||
}
|
||||
.urlRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.result {
|
||||
min-height: 1lh;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Appearance & Behavior pages, laid out like IntelliJ IDEA / PyCharm:
|
||||
* Appearance, System Settings (+ Language and Region, Date Formats) and
|
||||
* Appearance, System Settings (+ Language and Region, Date Formats, HTTP Proxy) and
|
||||
* Notifications. Every option is wired (see `appearanceRuntime`, workbench
|
||||
* theme / tool windows / notifications, app title / windows, `platform/dateFormat`).
|
||||
*/
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "../model/behaviorSchema";
|
||||
import { ACCENT_PRESETS, LIMITS, type ThemeId } from "../model/schema";
|
||||
import type { PageSpec } from "./spec";
|
||||
import { HTTP_PROXY_PAGE } from "./httpProxy";
|
||||
import styles from "./Behavior.module.css";
|
||||
import { tx, useT } from "@/platform/i18n";
|
||||
|
||||
@@ -673,6 +674,7 @@ export const BEHAVIOR_PAGES: PageSpec[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
HTTP_PROXY_PAGE,
|
||||
{
|
||||
id: "notifications",
|
||||
title: "Notifications",
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* Appearance & Behavior › System Settings › HTTP Proxy: how the IDE's own
|
||||
* connections (HTTP Client, tool / JDK / agent downloads, update checks) reach
|
||||
* the network. A manual proxy also reaches child processes as `HTTP_PROXY`.
|
||||
*/
|
||||
import { useId, useRef, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { call, commands, isTauri } from "@/platform/ipc";
|
||||
import { tx, useTx } from "@/platform/i18n";
|
||||
import {
|
||||
PROXY_PORT_LIMITS,
|
||||
proxyProblem,
|
||||
withProxyKind,
|
||||
type ProxyKindValid,
|
||||
type ProxyModeValid,
|
||||
type ProxySettingsValid,
|
||||
} from "@/platform/proxySettings";
|
||||
import { Button, Checkbox, Input, Popup, T } from "@/ui";
|
||||
import { NumberInput, Row, SearchLabel } from "../components/Form";
|
||||
import { usePageContext, useSetting } from "../components/context";
|
||||
import { stage } from "../model/staging";
|
||||
import type { PageSpec } from "./spec";
|
||||
import styles from "./HttpProxy.module.css";
|
||||
|
||||
const MODES: Array<{ value: ProxyModeValid; label: string; hint?: string }> = [
|
||||
{ value: "none", label: "No proxy" },
|
||||
{
|
||||
value: "auto",
|
||||
label: "Auto-detect proxy settings",
|
||||
hint: "Uses the system proxy: the HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment variables.",
|
||||
},
|
||||
{ value: "manual", label: "Manual proxy configuration" },
|
||||
];
|
||||
|
||||
const KINDS: Array<{ value: ProxyKindValid; label: string }> = [
|
||||
{ value: "http", label: "HTTP" },
|
||||
{ value: "socks", label: "SOCKS" },
|
||||
];
|
||||
|
||||
/** The staged proxy settings and a setter per field (each written as its own key). */
|
||||
function useProxy(): [ProxySettingsValid, (patch: Partial<ProxySettingsValid>) => void] {
|
||||
const [proxy] = useSetting<ProxySettingsValid>("proxy", "global");
|
||||
const { pageId } = usePageContext();
|
||||
const update = (patch: Partial<ProxySettingsValid>) => {
|
||||
for (const [field, value] of Object.entries(patch)) stage(pageId, "global", `proxy.${field}`, value);
|
||||
};
|
||||
return [proxy, update];
|
||||
}
|
||||
|
||||
function Radio({
|
||||
name,
|
||||
checked,
|
||||
onSelect,
|
||||
label,
|
||||
disabled,
|
||||
}: {
|
||||
name: string;
|
||||
checked: boolean;
|
||||
onSelect: () => void;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<label className={clsx(styles.radio, disabled && styles.disabled)}>
|
||||
<input type="radio" name={name} checked={checked} disabled={disabled} onChange={onSelect} />
|
||||
<SearchLabel text={label} />
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function ProxyOptions() {
|
||||
const [proxy, update] = useProxy();
|
||||
const name = useId();
|
||||
const kindName = useId();
|
||||
const ids = {
|
||||
host: useId(),
|
||||
port: useId(),
|
||||
noProxy: useId(),
|
||||
login: useId(),
|
||||
password: useId(),
|
||||
};
|
||||
const manual = proxy.mode === "manual";
|
||||
const auth = manual && proxy.authenticate;
|
||||
const problem = proxyProblem(proxy);
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.modes} role="radiogroup" aria-label={tx("Proxy")}>
|
||||
{MODES.map((m) => (
|
||||
<div key={m.value}>
|
||||
<Radio
|
||||
name={name}
|
||||
checked={proxy.mode === m.value}
|
||||
onSelect={() => update({ mode: m.value })}
|
||||
label={m.label}
|
||||
/>
|
||||
{m.hint && (
|
||||
<div className={styles.hint}>
|
||||
<T>{m.hint}</T>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.manual}>
|
||||
<div className={styles.kinds} role="radiogroup" aria-label={tx("Proxy type")}>
|
||||
{KINDS.map((k) => (
|
||||
<Radio
|
||||
key={k.value}
|
||||
name={kindName}
|
||||
checked={proxy.kind === k.value}
|
||||
disabled={!manual}
|
||||
onSelect={() => update(withProxyKind(proxy, k.value))}
|
||||
label={k.label}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.hostRow}>
|
||||
<Row label="Host name" htmlFor={ids.host}>
|
||||
<Input
|
||||
id={ids.host}
|
||||
className={styles.host}
|
||||
disabled={!manual}
|
||||
value={proxy.host}
|
||||
onChange={(e) => update({ host: e.target.value })}
|
||||
/>
|
||||
</Row>
|
||||
<Row label="Port number" htmlFor={ids.port}>
|
||||
{manual ? (
|
||||
<NumberInput
|
||||
id={ids.port}
|
||||
value={proxy.port}
|
||||
onChange={(port) => update({ port })}
|
||||
{...PROXY_PORT_LIMITS}
|
||||
integer
|
||||
aria-label="Port number"
|
||||
/>
|
||||
) : (
|
||||
<Input id={ids.port} className={styles.port} value={String(proxy.port)} disabled readOnly />
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
<Row
|
||||
label="No proxy for"
|
||||
htmlFor={ids.noProxy}
|
||||
hint="Comma-separated host names; * matches any characters, e.g. *.example.com, 192.168.*"
|
||||
>
|
||||
<Input
|
||||
id={ids.noProxy}
|
||||
className={styles.wide}
|
||||
disabled={!manual}
|
||||
value={proxy.noProxy}
|
||||
onChange={(e) => update({ noProxy: e.target.value })}
|
||||
/>
|
||||
</Row>
|
||||
<div className={styles.toggle}>
|
||||
<Checkbox
|
||||
checked={proxy.authenticate}
|
||||
disabled={!manual}
|
||||
onChange={(authenticate) => update({ authenticate })}
|
||||
label={<SearchLabel text="Proxy authentication" />}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.auth}>
|
||||
<Row label="Login" htmlFor={ids.login}>
|
||||
<Input
|
||||
id={ids.login}
|
||||
className={styles.host}
|
||||
disabled={!auth}
|
||||
value={proxy.login}
|
||||
onChange={(e) => update({ login: e.target.value })}
|
||||
/>
|
||||
</Row>
|
||||
<Row label="Password" htmlFor={ids.password}>
|
||||
<Input
|
||||
id={ids.password}
|
||||
type="password"
|
||||
className={styles.host}
|
||||
disabled={!auth}
|
||||
value={proxy.password}
|
||||
onChange={(e) => update({ password: e.target.value })}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
{problem && (
|
||||
<div className={styles.error} role="alert">
|
||||
<T>{problem}</T>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<CheckConnection proxy={proxy} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type CheckState = { status: "idle" | "running" } | { status: "ok" | "failed"; text: string };
|
||||
|
||||
/** Check connection…: asks for a URL and GETs it with the settings as edited. */
|
||||
function CheckConnection({ proxy }: { proxy: ProxySettingsValid }) {
|
||||
const t = useTx();
|
||||
const anchor = useRef<HTMLButtonElement>(null);
|
||||
const urlId = useId();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [url, setUrl] = useState("http://");
|
||||
const [state, setState] = useState<CheckState>({ status: "idle" });
|
||||
const check = async () => {
|
||||
setState({ status: "running" });
|
||||
try {
|
||||
const status = await call(commands.proxyCheckConnection(proxy, url));
|
||||
setState({ status: "ok", text: t`Connection successful: ${status}` });
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
setState({
|
||||
status: "failed",
|
||||
text: t`Problem with connection: ${message}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
const running = state.status === "running";
|
||||
return (
|
||||
<div className={styles.check}>
|
||||
<Button
|
||||
ref={anchor}
|
||||
size="small"
|
||||
disabled={!isTauri() || !!proxyProblem(proxy)}
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
>
|
||||
<T>Check connection…</T>
|
||||
</Button>
|
||||
<Popup
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
anchor={anchor}
|
||||
placement="bottom-start"
|
||||
offset={4}
|
||||
aria-label="Check connection"
|
||||
>
|
||||
<form
|
||||
className={styles.popup}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (!running && url.trim()) void check();
|
||||
}}
|
||||
>
|
||||
<label htmlFor={urlId}>
|
||||
<T>Enter any URL to check connection to:</T>
|
||||
</label>
|
||||
<span className={styles.urlRow}>
|
||||
<Input id={urlId} autoFocus className={styles.wide} value={url} onChange={(e) => setUrl(e.target.value)} />
|
||||
<Button size="small" type="submit" disabled={running || !url.trim()}>
|
||||
<T>Check</T>
|
||||
</Button>
|
||||
</span>
|
||||
<span
|
||||
role="status"
|
||||
className={clsx(
|
||||
styles.result,
|
||||
state.status === "ok" && styles.ok,
|
||||
state.status === "failed" && styles.error,
|
||||
)}
|
||||
>
|
||||
{state.status === "ok" || state.status === "failed" ? state.text : running ? t("Connecting…") : ""}
|
||||
</span>
|
||||
</form>
|
||||
</Popup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const HTTP_PROXY_PAGE: PageSpec = {
|
||||
id: "general.httpProxy",
|
||||
title: "HTTP Proxy",
|
||||
order: 2,
|
||||
keywords: ["proxy", "network", "socks", "http_proxy", "no_proxy", "connection"],
|
||||
sections: [
|
||||
{
|
||||
options: [
|
||||
{
|
||||
kind: "custom",
|
||||
label: "Manual proxy configuration",
|
||||
keywords: [
|
||||
"No proxy",
|
||||
"Auto-detect proxy settings",
|
||||
"Host name",
|
||||
"Port number",
|
||||
"No proxy for",
|
||||
"Proxy authentication",
|
||||
"Login",
|
||||
"Password",
|
||||
"Check connection…",
|
||||
],
|
||||
component: ProxyOptions,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
Reference in new issue
Заблокувати користувача