Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ base64 = "0.22"
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" }
ignore = "0.4.25"
portable-pty = "0.8"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream", "socks"] }
libc = "0.2"
chrono = { version = "0.4", features = ["clock"] }
shell-words = "1.1"
Expand Down
8 changes: 8 additions & 0 deletions src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,12 @@ pub(crate) struct AppSettings {
pub(crate) remote_backend_host: String,
#[serde(default, rename = "remoteBackendToken")]
pub(crate) remote_backend_token: Option<String>,
/// Single proxy URL used for outbound network requests.
///
/// Currently only applied to updater requests (check + download) on the frontend.
/// Supported schemes depend on the native HTTP client capabilities (e.g. `http(s)` and `socks5(h)`).
#[serde(default, rename = "proxyUrl")]
pub(crate) proxy_url: Option<String>,
#[serde(default = "default_access_mode", rename = "defaultAccessMode")]
pub(crate) default_access_mode: String,
#[serde(
Expand Down Expand Up @@ -710,6 +716,7 @@ impl Default for AppSettings {
backend_mode: BackendMode::Local,
remote_backend_host: default_remote_backend_host(),
remote_backend_token: None,
proxy_url: None,
default_access_mode: "current".to_string(),
composer_model_shortcut: default_composer_model_shortcut(),
composer_access_shortcut: default_composer_access_shortcut(),
Expand Down Expand Up @@ -772,6 +779,7 @@ mod tests {
assert!(matches!(settings.backend_mode, BackendMode::Local));
assert_eq!(settings.remote_backend_host, "127.0.0.1:4732");
assert!(settings.remote_backend_token.is_none());
assert!(settings.proxy_url.is_none());
assert_eq!(settings.default_access_mode, "current");
assert_eq!(
settings.composer_model_shortcut.as_deref(),
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ function MainApp() {
dismissUpdate,
handleTestNotificationSound,
} = useUpdaterController({
enabled: !appSettingsLoading,
proxyUrl: appSettings.proxyUrl,
notificationSoundsEnabled: appSettings.notificationSoundsEnabled,
onDebug: addDebugEntry,
successSoundUrl,
Expand Down
6 changes: 6 additions & 0 deletions src/features/app/hooks/useUpdaterController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,25 @@ import { subscribeUpdaterCheck } from "../../../services/events";
import type { DebugEntry } from "../../../types";

type Params = {
enabled?: boolean;
proxyUrl?: string | null;
notificationSoundsEnabled: boolean;
onDebug: (entry: DebugEntry) => void;
successSoundUrl: string;
errorSoundUrl: string;
};

export function useUpdaterController({
enabled = true,
proxyUrl,
notificationSoundsEnabled,
onDebug,
successSoundUrl,
errorSoundUrl,
}: Params) {
const { state: updaterState, startUpdate, checkForUpdates, dismiss } = useUpdater({
enabled,
proxyUrl,
onDebug,
});
const isWindowFocused = useWindowFocusState();
Expand Down
1 change: 1 addition & 0 deletions src/features/settings/components/SettingsView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const baseSettings: AppSettings = {
backendMode: "local",
remoteBackendHost: "127.0.0.1:4732",
remoteBackendToken: null,
proxyUrl: null,
defaultAccessMode: "current",
composerModelShortcut: null,
composerAccessShortcut: null,
Expand Down
81 changes: 80 additions & 1 deletion src/features/settings/components/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Trash2 from "lucide-react/dist/esm/icons/trash-2";
import X from "lucide-react/dist/esm/icons/x";
import FlaskConical from "lucide-react/dist/esm/icons/flask-conical";
import ExternalLink from "lucide-react/dist/esm/icons/external-link";
import Globe from "lucide-react/dist/esm/icons/globe";
import type {
AppSettings,
CodexDoctorResult,
Expand Down Expand Up @@ -171,7 +172,8 @@ type SettingsSection =
| "composer"
| "dictation"
| "shortcuts"
| "open-apps";
| "open-apps"
| "network";
type CodexSection = SettingsSection | "codex" | "experimental";
type ShortcutSettingKey =
| "composerModelShortcut"
Expand Down Expand Up @@ -276,6 +278,7 @@ export function SettingsView({
const [codexArgsDraft, setCodexArgsDraft] = useState(appSettings.codexArgs ?? "");
const [remoteHostDraft, setRemoteHostDraft] = useState(appSettings.remoteBackendHost);
const [remoteTokenDraft, setRemoteTokenDraft] = useState(appSettings.remoteBackendToken ?? "");
const [proxyUrlDraft, setProxyUrlDraft] = useState(appSettings.proxyUrl ?? "");
const [scaleDraft, setScaleDraft] = useState(
`${Math.round(clampUiScale(appSettings.uiScale) * 100)}%`,
);
Expand Down Expand Up @@ -386,6 +389,10 @@ export function SettingsView({
setRemoteTokenDraft(appSettings.remoteBackendToken ?? "");
}, [appSettings.remoteBackendToken]);

useEffect(() => {
setProxyUrlDraft(appSettings.proxyUrl ?? "");
}, [appSettings.proxyUrl]);

useEffect(() => {
setScaleDraft(`${Math.round(clampUiScale(appSettings.uiScale) * 100)}%`);
}, [appSettings.uiScale]);
Expand Down Expand Up @@ -546,6 +553,18 @@ export function SettingsView({
});
};

const handleCommitProxyUrl = async () => {
const nextProxyUrl = proxyUrlDraft.trim() ? proxyUrlDraft.trim() : null;
setProxyUrlDraft(nextProxyUrl ?? "");
if (nextProxyUrl === appSettings.proxyUrl) {
return;
}
await onUpdateAppSettings({
...appSettings,
proxyUrl: nextProxyUrl,
});
};

const handleCommitScale = async () => {
if (parsedScale === null) {
setScaleDraft(`${Math.round(clampUiScale(appSettings.uiScale) * 100)}%`);
Expand Down Expand Up @@ -983,6 +1002,14 @@ export function SettingsView({
<FlaskConical aria-hidden />
Experimental
</button>
<button
type="button"
className={`settings-nav ${activeSection === "network" ? "active" : ""}`}
onClick={() => setActiveSection("network")}
>
<Globe aria-hidden />
Network
</button>
</aside>
<div className="settings-content">
{activeSection === "projects" && (
Expand Down Expand Up @@ -2888,6 +2915,58 @@ export function SettingsView({
</div>
</section>
)}
{activeSection === "network" && (
<section className="settings-section">
<div className="settings-section-title">Network</div>
<div className="settings-section-subtitle">
Configure an outbound network proxy (currently used for updater requests).
</div>
<div className="settings-field">
<label className="settings-field-label" htmlFor="proxy-url">
Proxy
</label>
<div className="settings-field-row">
<input
id="proxy-url"
className="settings-input settings-input--compact"
value={proxyUrlDraft}
placeholder="http://127.0.0.1:7890"
onChange={(event) => setProxyUrlDraft(event.target.value)}
onBlur={() => {
void handleCommitProxyUrl();
}}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
void handleCommitProxyUrl();
}
}}
aria-label="Proxy URL"
/>
<button
type="button"
className="ghost settings-button-compact"
onClick={() => {
setProxyUrlDraft("");
void onUpdateAppSettings({
...appSettings,
proxyUrl: null,
});
}}
disabled={!appSettings.proxyUrl && !proxyUrlDraft.trim()}
>
Clear
</button>
</div>
<div className="settings-help">
Used for updater requests (check + download). Examples:
{" "}
<code>http://user:pass@host:port</code>,{" "}
<code>socks5h://127.0.0.1:7891</code>
</div>
</div>
</section>
)}
</div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/features/settings/hooks/useAppSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const defaultSettings: AppSettings = {
backendMode: "local",
remoteBackendHost: "127.0.0.1:4732",
remoteBackendToken: null,
proxyUrl: null,
defaultAccessMode: "current",
composerModelShortcut: "cmd+shift+m",
composerAccessShortcut: "cmd+shift+a",
Expand Down
8 changes: 5 additions & 3 deletions src/features/update/hooks/useUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ export type UpdateState = {

type UseUpdaterOptions = {
enabled?: boolean;
proxyUrl?: string | null;
onDebug?: (entry: DebugEntry) => void;
};

export function useUpdater({ enabled = true, onDebug }: UseUpdaterOptions) {
export function useUpdater({ enabled = true, proxyUrl, onDebug }: UseUpdaterOptions) {
const [state, setState] = useState<UpdateState>({ stage: "idle" });
const updateRef = useRef<Update | null>(null);
const latestTimeoutRef = useRef<number | null>(null);
Expand All @@ -58,7 +59,8 @@ export function useUpdater({ enabled = true, onDebug }: UseUpdaterOptions) {
try {
clearLatestTimeout();
setState({ stage: "checking" });
update = await check();
const trimmedProxyUrl = proxyUrl?.trim() ?? "";
update = await check(trimmedProxyUrl ? { proxy: trimmedProxyUrl } : undefined);
if (!update) {
Comment on lines 61 to 64
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-check update after proxy changes before download

The proxy is only applied in the check() call (plugin-updater’s CheckOptions is the only place that accepts a proxy; download/downloadAndInstall don’t take one), so if an update was already cached and the user changes the proxy, startUpdate will reuse the old Update instance and download with the previous proxy. This can cause downloads to keep failing behind a proxy unless the user manually re-checks; consider clearing updateRef or forcing a re-check whenever proxyUrl changes.

Useful? React with 👍 / 👎.

if (options?.announceNoUpdate) {
setState({ stage: "latest" });
Expand Down Expand Up @@ -93,7 +95,7 @@ export function useUpdater({ enabled = true, onDebug }: UseUpdaterOptions) {
await update?.close();
}
}
}, [clearLatestTimeout, onDebug]);
}, [clearLatestTimeout, onDebug, proxyUrl]);

const startUpdate = useCallback(async () => {
const update = updateRef.current;
Expand Down
10 changes: 10 additions & 0 deletions src/styles/settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,16 @@
color: var(--text-subtle);
}

.settings-help code {
font-family: "SFMono-Regular", "Menlo", "Monaco", monospace;
font-size: 11px;
padding: 1px 4px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--border-muted);
color: var(--text-strong);
}

.settings-download-progress {
display: flex;
flex-direction: column;
Expand Down
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ export type AppSettings = {
backendMode: BackendMode;
remoteBackendHost: string;
remoteBackendToken: string | null;
/**
* Single proxy URL used for outbound network requests.
*
* Currently only applied to updater requests (check + download).
* Supported schemes depend on the native client implementation:
* - http:// / https://
* - socks5:// / socks5h:// (when built with socks support)
*/
proxyUrl: string | null;
defaultAccessMode: AccessMode;
composerModelShortcut: string | null;
composerAccessShortcut: string | null;
Expand Down