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
44 changes: 43 additions & 1 deletion electron/main.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { app, BrowserWindow, nativeImage, dialog } from 'electron';
import { app, BrowserWindow, nativeImage, dialog, session } from 'electron';
import path from 'path';
import { spawn, execFileSync, ChildProcess } from 'child_process';
import fs from 'fs';
Expand DownExpand Up@@ -253,6 +253,46 @@ function getIconPath(): string {
return path.join(process.resourcesPath, 'icon.icns');
}

/**
* Fetch proxy settings from the CodePilot API and apply them
* to Electron's default session so renderer network requests
* (e.g. remote images in markdown) also route through the proxy.
*/
async function applyProxySettings(port: number): Promise<void> {
try {
const http = require('http');
const data: string = await new Promise((resolve, reject) => {
const req = http.get(`http://127.0.0.1:${port}/api/settings/app`, (res: { statusCode?: number; on: Function }) => {
let body = '';
res.on('data', (chunk: string) => { body += chunk; });
res.on('end', () => resolve(body));
});
req.on('error', reject);
req.setTimeout(3000, () => { req.destroy(); reject(new Error('timeout')); });
});

const parsed = JSON.parse(data);
const settings = parsed?.settings || {};

if (settings.proxy_enabled === 'true' && settings.proxy_url) {
const proxyUrl = settings.proxy_url;
const bypass = settings.proxy_bypass || 'localhost,127.0.0.1';
// Electron's setProxy expects proxyRules in the format: scheme://host:port
await session.defaultSession.setProxy({
proxyRules: proxyUrl,
proxyBypassRules: bypass,
});
console.log(`[proxy] Electron session proxy set to ${proxyUrl} (bypass: ${bypass})`);
} else {
// Ensure proxy is cleared if disabled
await session.defaultSession.setProxy({ mode: 'direct' as never });
console.log('[proxy] Electron session proxy: direct (no proxy)');
}
} catch (err) {
console.warn('[proxy] Failed to apply Electron session proxy:', err);
}
}

function createWindow(port: number) {
const windowOptions: Electron.BrowserWindowConstructorOptions = {
width: 1280,
Expand DownExpand Up@@ -319,6 +359,7 @@ app.whenReady().then(async () => {
}

serverPort = port;
await applyProxySettings(port);
createWindow(port);
} catch (err) {
console.error('Failed to start:', err);
Expand DownExpand Up@@ -349,6 +390,7 @@ app.on('activate', async () => {
await waitForServer(port);
serverPort = port;
}
await applyProxySettings(serverPort || 3000);
createWindow(serverPort || 3000);
} catch (err) {
console.error('Failed to restart server:', err);
Expand Down
100 changes: 100 additions & 0 deletions scripts/test-proxy.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env node
/**
* Minimal HTTP CONNECT proxy for testing the proxy feature.
*
* Usage:
* 1. node scripts/test-proxy.mjs (starts on port 9999)
* 2. In CodePilot Settings, set proxy to http://127.0.0.1:9999, enable it
* 3. Send a message in chat
* 4. Watch this terminal — you should see CONNECT requests to api.anthropic.com
*
* Press Ctrl+C to stop.
*/

import http from 'node:http';
import net from 'node:net';
import { URL } from 'node:url';

const PORT = parseInt(process.argv[2] || '9999', 10);
let requestCount = 0;

const server = http.createServer((req, res) => {
// Handle normal HTTP proxy requests (non-CONNECT)
requestCount++;
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] #${requestCount} HTTP ${req.method} ${req.url}`);

try {
const target = new URL(req.url);
const proxyReq = http.request(
{
hostname: target.hostname,
port: target.port || 80,
path: target.pathname + target.search,
method: req.method,
headers: req.headers,
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
}
);
proxyReq.on('error', (err) => {
console.log(` -> ERROR: ${err.message}`);
res.writeHead(502);
res.end('Bad Gateway');
});
req.pipe(proxyReq);
} catch (err) {
res.writeHead(400);
res.end('Bad Request');
}
});

// Handle HTTPS CONNECT tunneling
server.on('connect', (req, clientSocket, head) => {
requestCount++;
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] #${requestCount} CONNECT ${req.url}`);

const [hostname, port] = req.url.split(':');
const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
clientSocket.write(
'HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: codepilot-test-proxy\r\n' +
'\r\n'
);
serverSocket.write(head);
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
});

serverSocket.on('error', (err) => {
console.log(` -> CONNECT ERROR to ${req.url}: ${err.message}`);
clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
clientSocket.end();
});

clientSocket.on('error', () => serverSocket.destroy());
});

server.listen(PORT, '127.0.0.1', () => {
console.log('');
console.log('='.repeat(60));
console.log(' CodePilot Test Proxy');
console.log('='.repeat(60));
console.log(` Listening on: http://127.0.0.1:${PORT}`);
console.log('');
console.log(' Steps:');
console.log(` 1. Open CodePilot Settings`);
console.log(` 2. Enable proxy, set URL to http://127.0.0.1:${PORT}`);
console.log(` 3. Save, then send a chat message`);
console.log(` 4. Watch this terminal for CONNECT/HTTP requests`);
console.log('');
console.log(' Expected output:');
console.log(' CONNECT api.anthropic.com:443');
console.log('');
console.log(' Press Ctrl+C to stop');
console.log('='.repeat(60));
console.log('');
});
3 changes: 3 additions & 0 deletions src/app/api/settings/app/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,9 @@ import { getSetting, setSetting } from '@/lib/db';
const ALLOWED_KEYS = [
'anthropic_auth_token',
'anthropic_base_url',
'proxy_enabled',
'proxy_url',
'proxy_bypass',
];

export async function GET() {
Expand Down
131 changes: 131 additions & 0 deletions src/app/settings/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ import {
CodeIcon,
SlidersHorizontalIcon,
Loading02Icon,
GlobeIcon,
} from "@hugeicons/core-free-icons";

interface SettingsData {
Expand DownExpand Up@@ -165,6 +166,135 @@ function ApiConfigSection() {
);
}

// --- Proxy Configuration Section (CodePilot app settings, stored in SQLite) ---
function ProxyConfigSection() {
const [proxyEnabled, setProxyEnabled] = useState(false);
const [proxyUrl, setProxyUrl] = useState("");
const [proxyBypass, setProxyBypass] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [status, setStatus] = useState<"idle" | "saved" | "error">("idle");

useEffect(() => {
fetch("/api/settings/app")
.then((r) => r.json())
.then((data) => {
const s = data.settings || {};
setProxyEnabled(s.proxy_enabled === "true");
setProxyUrl(s.proxy_url || "");
setProxyBypass(s.proxy_bypass || "");
})
.catch(() => {})
.finally(() => setLoading(false));
}, []);

const handleSave = async () => {
setSaving(true);
setStatus("idle");
try {
const res = await fetch("/api/settings/app", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
settings: {
proxy_enabled: proxyEnabled ? "true" : "false",
proxy_url: proxyUrl,
proxy_bypass: proxyBypass,
},
}),
});
if (res.ok) {
setStatus("saved");
setTimeout(() => setStatus("idle"), 2000);
} else {
setStatus("error");
}
} catch {
setStatus("error");
} finally {
setSaving(false);
}
};

if (loading) return null;

return (
<div className="rounded-lg border border-border/50 p-4 space-y-4">
<div>
<div className="flex items-center gap-2">
<HugeiconsIcon icon={GlobeIcon} className="h-4 w-4 text-muted-foreground" />
<Label className="text-sm font-medium">Proxy Configuration</Label>
</div>
<p className="text-xs text-muted-foreground mt-1">
Configure an HTTP/HTTPS/SOCKS5 proxy for all network access. The proxy
environment variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY) will be
passed to the Claude Code subprocess.
</p>
</div>
<div className="flex items-center gap-3">
<Switch
id="proxy-enabled"
checked={proxyEnabled}
onCheckedChange={setProxyEnabled}
/>
<Label htmlFor="proxy-enabled" className="text-sm">
{proxyEnabled ? "Proxy Enabled" : "Proxy Disabled"}
</Label>
</div>
<div className="space-y-3">
<div>
<Label htmlFor="proxy-url" className="text-xs text-muted-foreground">
Proxy URL
</Label>
<Input
id="proxy-url"
placeholder="http://127.0.0.1:7890"
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
disabled={!proxyEnabled}
className="mt-1 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Supports http://, https://, socks5:// protocols
</p>
</div>
<div>
<Label htmlFor="proxy-bypass" className="text-xs text-muted-foreground">
Bypass List (NO_PROXY)
</Label>
<Input
id="proxy-bypass"
placeholder="localhost,127.0.0.1,::1"
value={proxyBypass}
onChange={(e) => setProxyBypass(e.target.value)}
disabled={!proxyEnabled}
className="mt-1 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Comma-separated list of hosts that should bypass the proxy
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Button onClick={handleSave} disabled={saving} size="sm" className="gap-2">
{saving ? (
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
) : (
<HugeiconsIcon icon={FloppyDiskIcon} className="h-4 w-4" />
)}
{saving ? "Saving..." : "Save Proxy Config"}
</Button>
{status === "saved" && (
<span className="text-sm text-green-600 dark:text-green-400">Saved</span>
)}
{status === "error" && (
<span className="text-sm text-destructive">Failed to save</span>
)}
</div>
</div>
);
}

// --- Claude CLI Settings Section (manages ~/.claude/settings.json) ---
function SettingsPageInner() {
const [settings, setSettings] = useState<SettingsData>({});
Expand DownExpand Up@@ -281,6 +411,7 @@ function SettingsPageInner() {
<div className="flex-1 overflow-auto p-6">
<div className="max-w-3xl space-y-6">
<ApiConfigSection />
<ProxyConfigSection />

{loading ? (
<div className="flex items-center justify-center py-12">
Expand Down
19 changes: 19 additions & 0 deletions src/lib/claude-client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,25 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
sdkEnv.ANTHROPIC_BASE_URL = appBaseUrl;
}

// Inject proxy environment variables if proxy is enabled
const proxyEnabled = getSetting('proxy_enabled');
if (proxyEnabled === 'true') {
const proxyUrl = getSetting('proxy_url');
const proxyBypass = getSetting('proxy_bypass');
if (proxyUrl) {
sdkEnv.HTTP_PROXY = proxyUrl;
sdkEnv.HTTPS_PROXY = proxyUrl;
sdkEnv.ALL_PROXY = proxyUrl;
sdkEnv.http_proxy = proxyUrl;
sdkEnv.https_proxy = proxyUrl;
sdkEnv.all_proxy = proxyUrl;
}
if (proxyBypass) {
sdkEnv.NO_PROXY = proxyBypass;
sdkEnv.no_proxy = proxyBypass;
}
}

const queryOptions: Options = {
cwd: workingDirectory || process.cwd(),
abortController,
Expand Down
3 changes: 3 additions & 0 deletions src/types/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -345,6 +345,9 @@ export const SETTING_KEYS = {
THEME: 'theme',
PERMISSION_MODE: 'permission_mode',
MAX_THINKING_TOKENS: 'max_thinking_tokens',
PROXY_ENABLED: 'proxy_enabled',
PROXY_URL: 'proxy_url',
PROXY_BYPASS: 'proxy_bypass',
} as const;

// ==========================================
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
44 changes: 43 additions & 1 deletion electron/main.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { app, BrowserWindow, nativeImage, dialog } from 'electron';
import { app, BrowserWindow, nativeImage, dialog, session } from 'electron';
import path from 'path';
import { spawn, execFileSync, ChildProcess } from 'child_process';
import fs from 'fs';
Expand DownExpand Up@@ -253,6 +253,46 @@ function getIconPath(): string {
return path.join(process.resourcesPath, 'icon.icns');
}

/**
* Fetch proxy settings from the CodePilot API and apply them
* to Electron's default session so renderer network requests
* (e.g. remote images in markdown) also route through the proxy.
*/
async function applyProxySettings(port: number): Promise<void> {
try {
const http = require('http');
const data: string = await new Promise((resolve, reject) => {
const req = http.get(`http://127.0.0.1:${port}/api/settings/app`, (res: { statusCode?: number; on: Function }) => {
let body = '';
res.on('data', (chunk: string) => { body += chunk; });
res.on('end', () => resolve(body));
});
req.on('error', reject);
req.setTimeout(3000, () => { req.destroy(); reject(new Error('timeout')); });
});

const parsed = JSON.parse(data);
const settings = parsed?.settings || {};

if (settings.proxy_enabled === 'true' && settings.proxy_url) {
const proxyUrl = settings.proxy_url;
const bypass = settings.proxy_bypass || 'localhost,127.0.0.1';
// Electron's setProxy expects proxyRules in the format: scheme://host:port
await session.defaultSession.setProxy({
proxyRules: proxyUrl,
proxyBypassRules: bypass,
});
console.log(`[proxy] Electron session proxy set to ${proxyUrl} (bypass: ${bypass})`);
} else {
// Ensure proxy is cleared if disabled
await session.defaultSession.setProxy({ mode: 'direct' as never });
console.log('[proxy] Electron session proxy: direct (no proxy)');
}
} catch (err) {
console.warn('[proxy] Failed to apply Electron session proxy:', err);
}
}

function createWindow(port: number) {
const windowOptions: Electron.BrowserWindowConstructorOptions = {
width: 1280,
Expand DownExpand Up@@ -319,6 +359,7 @@ app.whenReady().then(async () => {
}

serverPort = port;
await applyProxySettings(port);
createWindow(port);
} catch (err) {
console.error('Failed to start:', err);
Expand DownExpand Up@@ -349,6 +390,7 @@ app.on('activate', async () => {
await waitForServer(port);
serverPort = port;
}
await applyProxySettings(serverPort || 3000);
createWindow(serverPort || 3000);
} catch (err) {
console.error('Failed to restart server:', err);
Expand Down
100 changes: 100 additions & 0 deletions scripts/test-proxy.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env node
/**
* Minimal HTTP CONNECT proxy for testing the proxy feature.
*
* Usage:
* 1. node scripts/test-proxy.mjs (starts on port 9999)
* 2. In CodePilot Settings, set proxy to http://127.0.0.1:9999, enable it
* 3. Send a message in chat
* 4. Watch this terminal — you should see CONNECT requests to api.anthropic.com
*
* Press Ctrl+C to stop.
*/

import http from 'node:http';
import net from 'node:net';
import { URL } from 'node:url';

const PORT = parseInt(process.argv[2] || '9999', 10);
let requestCount = 0;

const server = http.createServer((req, res) => {
// Handle normal HTTP proxy requests (non-CONNECT)
requestCount++;
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] #${requestCount} HTTP ${req.method} ${req.url}`);

try {
const target = new URL(req.url);
const proxyReq = http.request(
{
hostname: target.hostname,
port: target.port || 80,
path: target.pathname + target.search,
method: req.method,
headers: req.headers,
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
}
);
proxyReq.on('error', (err) => {
console.log(` -> ERROR: ${err.message}`);
res.writeHead(502);
res.end('Bad Gateway');
});
req.pipe(proxyReq);
} catch (err) {
res.writeHead(400);
res.end('Bad Request');
}
});

// Handle HTTPS CONNECT tunneling
server.on('connect', (req, clientSocket, head) => {
requestCount++;
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] #${requestCount} CONNECT ${req.url}`);

const [hostname, port] = req.url.split(':');
const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
clientSocket.write(
'HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: codepilot-test-proxy\r\n' +
'\r\n'
);
serverSocket.write(head);
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
});

serverSocket.on('error', (err) => {
console.log(` -> CONNECT ERROR to ${req.url}: ${err.message}`);
clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
clientSocket.end();
});

clientSocket.on('error', () => serverSocket.destroy());
});

server.listen(PORT, '127.0.0.1', () => {
console.log('');
console.log('='.repeat(60));
console.log(' CodePilot Test Proxy');
console.log('='.repeat(60));
console.log(` Listening on: http://127.0.0.1:${PORT}`);
console.log('');
console.log(' Steps:');
console.log(` 1. Open CodePilot Settings`);
console.log(` 2. Enable proxy, set URL to http://127.0.0.1:${PORT}`);
console.log(` 3. Save, then send a chat message`);
console.log(` 4. Watch this terminal for CONNECT/HTTP requests`);
console.log('');
console.log(' Expected output:');
console.log(' CONNECT api.anthropic.com:443');
console.log('');
console.log(' Press Ctrl+C to stop');
console.log('='.repeat(60));
console.log('');
});
3 changes: 3 additions & 0 deletions src/app/api/settings/app/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,9 @@ import { getSetting, setSetting } from '@/lib/db';
const ALLOWED_KEYS = [
'anthropic_auth_token',
'anthropic_base_url',
'proxy_enabled',
'proxy_url',
'proxy_bypass',
];

export async function GET() {
Expand Down
131 changes: 131 additions & 0 deletions src/app/settings/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ import {
CodeIcon,
SlidersHorizontalIcon,
Loading02Icon,
GlobeIcon,
} from "@hugeicons/core-free-icons";

interface SettingsData {
Expand DownExpand Up@@ -165,6 +166,135 @@ function ApiConfigSection() {
);
}

// --- Proxy Configuration Section (CodePilot app settings, stored in SQLite) ---
function ProxyConfigSection() {
const [proxyEnabled, setProxyEnabled] = useState(false);
const [proxyUrl, setProxyUrl] = useState("");
const [proxyBypass, setProxyBypass] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [status, setStatus] = useState<"idle" | "saved" | "error">("idle");

useEffect(() => {
fetch("/api/settings/app")
.then((r) => r.json())
.then((data) => {
const s = data.settings || {};
setProxyEnabled(s.proxy_enabled === "true");
setProxyUrl(s.proxy_url || "");
setProxyBypass(s.proxy_bypass || "");
})
.catch(() => {})
.finally(() => setLoading(false));
}, []);

const handleSave = async () => {
setSaving(true);
setStatus("idle");
try {
const res = await fetch("/api/settings/app", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
settings: {
proxy_enabled: proxyEnabled ? "true" : "false",
proxy_url: proxyUrl,
proxy_bypass: proxyBypass,
},
}),
});
if (res.ok) {
setStatus("saved");
setTimeout(() => setStatus("idle"), 2000);
} else {
setStatus("error");
}
} catch {
setStatus("error");
} finally {
setSaving(false);
}
};

if (loading) return null;

return (
<div className="rounded-lg border border-border/50 p-4 space-y-4">
<div>
<div className="flex items-center gap-2">
<HugeiconsIcon icon={GlobeIcon} className="h-4 w-4 text-muted-foreground" />
<Label className="text-sm font-medium">Proxy Configuration</Label>
</div>
<p className="text-xs text-muted-foreground mt-1">
Configure an HTTP/HTTPS/SOCKS5 proxy for all network access. The proxy
environment variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY) will be
passed to the Claude Code subprocess.
</p>
</div>
<div className="flex items-center gap-3">
<Switch
id="proxy-enabled"
checked={proxyEnabled}
onCheckedChange={setProxyEnabled}
/>
<Label htmlFor="proxy-enabled" className="text-sm">
{proxyEnabled ? "Proxy Enabled" : "Proxy Disabled"}
</Label>
</div>
<div className="space-y-3">
<div>
<Label htmlFor="proxy-url" className="text-xs text-muted-foreground">
Proxy URL
</Label>
<Input
id="proxy-url"
placeholder="http://127.0.0.1:7890"
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
disabled={!proxyEnabled}
className="mt-1 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Supports http://, https://, socks5:// protocols
</p>
</div>
<div>
<Label htmlFor="proxy-bypass" className="text-xs text-muted-foreground">
Bypass List (NO_PROXY)
</Label>
<Input
id="proxy-bypass"
placeholder="localhost,127.0.0.1,::1"
value={proxyBypass}
onChange={(e) => setProxyBypass(e.target.value)}
disabled={!proxyEnabled}
className="mt-1 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Comma-separated list of hosts that should bypass the proxy
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Button onClick={handleSave} disabled={saving} size="sm" className="gap-2">
{saving ? (
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
) : (
<HugeiconsIcon icon={FloppyDiskIcon} className="h-4 w-4" />
)}
{saving ? "Saving..." : "Save Proxy Config"}
</Button>
{status === "saved" && (
<span className="text-sm text-green-600 dark:text-green-400">Saved</span>
)}
{status === "error" && (
<span className="text-sm text-destructive">Failed to save</span>
)}
</div>
</div>
);
}

// --- Claude CLI Settings Section (manages ~/.claude/settings.json) ---
function SettingsPageInner() {
const [settings, setSettings] = useState<SettingsData>({});
Expand DownExpand Up@@ -281,6 +411,7 @@ function SettingsPageInner() {
<div className="flex-1 overflow-auto p-6">
<div className="max-w-3xl space-y-6">
<ApiConfigSection />
<ProxyConfigSection />

{loading ? (
<div className="flex items-center justify-center py-12">
Expand Down
19 changes: 19 additions & 0 deletions src/lib/claude-client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,25 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
sdkEnv.ANTHROPIC_BASE_URL = appBaseUrl;
}

// Inject proxy environment variables if proxy is enabled
const proxyEnabled = getSetting('proxy_enabled');
if (proxyEnabled === 'true') {
const proxyUrl = getSetting('proxy_url');
const proxyBypass = getSetting('proxy_bypass');
if (proxyUrl) {
sdkEnv.HTTP_PROXY = proxyUrl;
sdkEnv.HTTPS_PROXY = proxyUrl;
sdkEnv.ALL_PROXY = proxyUrl;
sdkEnv.http_proxy = proxyUrl;
sdkEnv.https_proxy = proxyUrl;
sdkEnv.all_proxy = proxyUrl;
}
if (proxyBypass) {
sdkEnv.NO_PROXY = proxyBypass;
sdkEnv.no_proxy = proxyBypass;
}
}

const queryOptions: Options = {
cwd: workingDirectory || process.cwd(),
abortController,
Expand Down
3 changes: 3 additions & 0 deletions src/types/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -345,6 +345,9 @@ export const SETTING_KEYS = {
THEME: 'theme',
PERMISSION_MODE: 'permission_mode',
MAX_THINKING_TOKENS: 'max_thinking_tokens',
PROXY_ENABLED: 'proxy_enabled',
PROXY_URL: 'proxy_url',
PROXY_BYPASS: 'proxy_bypass',
} as const;

// ==========================================
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
44 changes: 43 additions & 1 deletion electron/main.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { app, BrowserWindow, nativeImage, dialog } from 'electron';
import { app, BrowserWindow, nativeImage, dialog, session } from 'electron';
import path from 'path';
import { spawn, execFileSync, ChildProcess } from 'child_process';
import fs from 'fs';
Expand DownExpand Up@@ -253,6 +253,46 @@ function getIconPath(): string {
return path.join(process.resourcesPath, 'icon.icns');
}

/**
* Fetch proxy settings from the CodePilot API and apply them
* to Electron's default session so renderer network requests
* (e.g. remote images in markdown) also route through the proxy.
*/
async function applyProxySettings(port: number): Promise<void> {
try {
const http = require('http');
const data: string = await new Promise((resolve, reject) => {
const req = http.get(`http://127.0.0.1:${port}/api/settings/app`, (res: { statusCode?: number; on: Function }) => {
let body = '';
res.on('data', (chunk: string) => { body += chunk; });
res.on('end', () => resolve(body));
});
req.on('error', reject);
req.setTimeout(3000, () => { req.destroy(); reject(new Error('timeout')); });
});

const parsed = JSON.parse(data);
const settings = parsed?.settings || {};

if (settings.proxy_enabled === 'true' && settings.proxy_url) {
const proxyUrl = settings.proxy_url;
const bypass = settings.proxy_bypass || 'localhost,127.0.0.1';
// Electron's setProxy expects proxyRules in the format: scheme://host:port
await session.defaultSession.setProxy({
proxyRules: proxyUrl,
proxyBypassRules: bypass,
});
console.log(`[proxy] Electron session proxy set to ${proxyUrl} (bypass: ${bypass})`);
} else {
// Ensure proxy is cleared if disabled
await session.defaultSession.setProxy({ mode: 'direct' as never });
console.log('[proxy] Electron session proxy: direct (no proxy)');
}
} catch (err) {
console.warn('[proxy] Failed to apply Electron session proxy:', err);
}
}

function createWindow(port: number) {
const windowOptions: Electron.BrowserWindowConstructorOptions = {
width: 1280,
Expand DownExpand Up@@ -319,6 +359,7 @@ app.whenReady().then(async () => {
}

serverPort = port;
await applyProxySettings(port);
createWindow(port);
} catch (err) {
console.error('Failed to start:', err);
Expand DownExpand Up@@ -349,6 +390,7 @@ app.on('activate', async () => {
await waitForServer(port);
serverPort = port;
}
await applyProxySettings(serverPort || 3000);
createWindow(serverPort || 3000);
} catch (err) {
console.error('Failed to restart server:', err);
Expand Down
100 changes: 100 additions & 0 deletions scripts/test-proxy.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env node
/**
* Minimal HTTP CONNECT proxy for testing the proxy feature.
*
* Usage:
* 1. node scripts/test-proxy.mjs (starts on port 9999)
* 2. In CodePilot Settings, set proxy to http://127.0.0.1:9999, enable it
* 3. Send a message in chat
* 4. Watch this terminal — you should see CONNECT requests to api.anthropic.com
*
* Press Ctrl+C to stop.
*/

import http from 'node:http';
import net from 'node:net';
import { URL } from 'node:url';

const PORT = parseInt(process.argv[2] || '9999', 10);
let requestCount = 0;

const server = http.createServer((req, res) => {
// Handle normal HTTP proxy requests (non-CONNECT)
requestCount++;
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] #${requestCount} HTTP ${req.method} ${req.url}`);

try {
const target = new URL(req.url);
const proxyReq = http.request(
{
hostname: target.hostname,
port: target.port || 80,
path: target.pathname + target.search,
method: req.method,
headers: req.headers,
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
}
);
proxyReq.on('error', (err) => {
console.log(` -> ERROR: ${err.message}`);
res.writeHead(502);
res.end('Bad Gateway');
});
req.pipe(proxyReq);
} catch (err) {
res.writeHead(400);
res.end('Bad Request');
}
});

// Handle HTTPS CONNECT tunneling
server.on('connect', (req, clientSocket, head) => {
requestCount++;
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] #${requestCount} CONNECT ${req.url}`);

const [hostname, port] = req.url.split(':');
const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
clientSocket.write(
'HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: codepilot-test-proxy\r\n' +
'\r\n'
);
serverSocket.write(head);
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
});

serverSocket.on('error', (err) => {
console.log(` -> CONNECT ERROR to ${req.url}: ${err.message}`);
clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
clientSocket.end();
});

clientSocket.on('error', () => serverSocket.destroy());
});

server.listen(PORT, '127.0.0.1', () => {
console.log('');
console.log('='.repeat(60));
console.log(' CodePilot Test Proxy');
console.log('='.repeat(60));
console.log(` Listening on: http://127.0.0.1:${PORT}`);
console.log('');
console.log(' Steps:');
console.log(` 1. Open CodePilot Settings`);
console.log(` 2. Enable proxy, set URL to http://127.0.0.1:${PORT}`);
console.log(` 3. Save, then send a chat message`);
console.log(` 4. Watch this terminal for CONNECT/HTTP requests`);
console.log('');
console.log(' Expected output:');
console.log(' CONNECT api.anthropic.com:443');
console.log('');
console.log(' Press Ctrl+C to stop');
console.log('='.repeat(60));
console.log('');
});
3 changes: 3 additions & 0 deletions src/app/api/settings/app/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,9 @@ import { getSetting, setSetting } from '@/lib/db';
const ALLOWED_KEYS = [
'anthropic_auth_token',
'anthropic_base_url',
'proxy_enabled',
'proxy_url',
'proxy_bypass',
];

export async function GET() {
Expand Down
131 changes: 131 additions & 0 deletions src/app/settings/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ import {
CodeIcon,
SlidersHorizontalIcon,
Loading02Icon,
GlobeIcon,
} from "@hugeicons/core-free-icons";

interface SettingsData {
Expand DownExpand Up@@ -165,6 +166,135 @@ function ApiConfigSection() {
);
}

// --- Proxy Configuration Section (CodePilot app settings, stored in SQLite) ---
function ProxyConfigSection() {
const [proxyEnabled, setProxyEnabled] = useState(false);
const [proxyUrl, setProxyUrl] = useState("");
const [proxyBypass, setProxyBypass] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [status, setStatus] = useState<"idle" | "saved" | "error">("idle");

useEffect(() => {
fetch("/api/settings/app")
.then((r) => r.json())
.then((data) => {
const s = data.settings || {};
setProxyEnabled(s.proxy_enabled === "true");
setProxyUrl(s.proxy_url || "");
setProxyBypass(s.proxy_bypass || "");
})
.catch(() => {})
.finally(() => setLoading(false));
}, []);

const handleSave = async () => {
setSaving(true);
setStatus("idle");
try {
const res = await fetch("/api/settings/app", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
settings: {
proxy_enabled: proxyEnabled ? "true" : "false",
proxy_url: proxyUrl,
proxy_bypass: proxyBypass,
},
}),
});
if (res.ok) {
setStatus("saved");
setTimeout(() => setStatus("idle"), 2000);
} else {
setStatus("error");
}
} catch {
setStatus("error");
} finally {
setSaving(false);
}
};

if (loading) return null;

return (
<div className="rounded-lg border border-border/50 p-4 space-y-4">
<div>
<div className="flex items-center gap-2">
<HugeiconsIcon icon={GlobeIcon} className="h-4 w-4 text-muted-foreground" />
<Label className="text-sm font-medium">Proxy Configuration</Label>
</div>
<p className="text-xs text-muted-foreground mt-1">
Configure an HTTP/HTTPS/SOCKS5 proxy for all network access. The proxy
environment variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY) will be
passed to the Claude Code subprocess.
</p>
</div>
<div className="flex items-center gap-3">
<Switch
id="proxy-enabled"
checked={proxyEnabled}
onCheckedChange={setProxyEnabled}
/>
<Label htmlFor="proxy-enabled" className="text-sm">
{proxyEnabled ? "Proxy Enabled" : "Proxy Disabled"}
</Label>
</div>
<div className="space-y-3">
<div>
<Label htmlFor="proxy-url" className="text-xs text-muted-foreground">
Proxy URL
</Label>
<Input
id="proxy-url"
placeholder="http://127.0.0.1:7890"
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
disabled={!proxyEnabled}
className="mt-1 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Supports http://, https://, socks5:// protocols
</p>
</div>
<div>
<Label htmlFor="proxy-bypass" className="text-xs text-muted-foreground">
Bypass List (NO_PROXY)
</Label>
<Input
id="proxy-bypass"
placeholder="localhost,127.0.0.1,::1"
value={proxyBypass}
onChange={(e) => setProxyBypass(e.target.value)}
disabled={!proxyEnabled}
className="mt-1 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Comma-separated list of hosts that should bypass the proxy
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Button onClick={handleSave} disabled={saving} size="sm" className="gap-2">
{saving ? (
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
) : (
<HugeiconsIcon icon={FloppyDiskIcon} className="h-4 w-4" />
)}
{saving ? "Saving..." : "Save Proxy Config"}
</Button>
{status === "saved" && (
<span className="text-sm text-green-600 dark:text-green-400">Saved</span>
)}
{status === "error" && (
<span className="text-sm text-destructive">Failed to save</span>
)}
</div>
</div>
);
}

// --- Claude CLI Settings Section (manages ~/.claude/settings.json) ---
function SettingsPageInner() {
const [settings, setSettings] = useState<SettingsData>({});
Expand DownExpand Up@@ -281,6 +411,7 @@ function SettingsPageInner() {
<div className="flex-1 overflow-auto p-6">
<div className="max-w-3xl space-y-6">
<ApiConfigSection />
<ProxyConfigSection />

{loading ? (
<div className="flex items-center justify-center py-12">
Expand Down
19 changes: 19 additions & 0 deletions src/lib/claude-client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,25 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
sdkEnv.ANTHROPIC_BASE_URL = appBaseUrl;
}

// Inject proxy environment variables if proxy is enabled
const proxyEnabled = getSetting('proxy_enabled');
if (proxyEnabled === 'true') {
const proxyUrl = getSetting('proxy_url');
const proxyBypass = getSetting('proxy_bypass');
if (proxyUrl) {
sdkEnv.HTTP_PROXY = proxyUrl;
sdkEnv.HTTPS_PROXY = proxyUrl;
sdkEnv.ALL_PROXY = proxyUrl;
sdkEnv.http_proxy = proxyUrl;
sdkEnv.https_proxy = proxyUrl;
sdkEnv.all_proxy = proxyUrl;
}
if (proxyBypass) {
sdkEnv.NO_PROXY = proxyBypass;
sdkEnv.no_proxy = proxyBypass;
}
}

const queryOptions: Options = {
cwd: workingDirectory || process.cwd(),
abortController,
Expand Down
3 changes: 3 additions & 0 deletions src/types/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -345,6 +345,9 @@ export const SETTING_KEYS = {
THEME: 'theme',
PERMISSION_MODE: 'permission_mode',
MAX_THINKING_TOKENS: 'max_thinking_tokens',
PROXY_ENABLED: 'proxy_enabled',
PROXY_URL: 'proxy_url',
PROXY_BYPASS: 'proxy_bypass',
} as const;

// ==========================================
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
44 changes: 43 additions & 1 deletion electron/main.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { app, BrowserWindow, nativeImage, dialog } from 'electron';
import { app, BrowserWindow, nativeImage, dialog, session } from 'electron';
import path from 'path';
import { spawn, execFileSync, ChildProcess } from 'child_process';
import fs from 'fs';
Expand DownExpand Up@@ -253,6 +253,46 @@ function getIconPath(): string {
return path.join(process.resourcesPath, 'icon.icns');
}

/**
* Fetch proxy settings from the CodePilot API and apply them
* to Electron's default session so renderer network requests
* (e.g. remote images in markdown) also route through the proxy.
*/
async function applyProxySettings(port: number): Promise<void> {
try {
const http = require('http');
const data: string = await new Promise((resolve, reject) => {
const req = http.get(`http://127.0.0.1:${port}/api/settings/app`, (res: { statusCode?: number; on: Function }) => {
let body = '';
res.on('data', (chunk: string) => { body += chunk; });
res.on('end', () => resolve(body));
});
req.on('error', reject);
req.setTimeout(3000, () => { req.destroy(); reject(new Error('timeout')); });
});

const parsed = JSON.parse(data);
const settings = parsed?.settings || {};

if (settings.proxy_enabled === 'true' && settings.proxy_url) {
const proxyUrl = settings.proxy_url;
const bypass = settings.proxy_bypass || 'localhost,127.0.0.1';
// Electron's setProxy expects proxyRules in the format: scheme://host:port
await session.defaultSession.setProxy({
proxyRules: proxyUrl,
proxyBypassRules: bypass,
});
console.log(`[proxy] Electron session proxy set to ${proxyUrl} (bypass: ${bypass})`);
} else {
// Ensure proxy is cleared if disabled
await session.defaultSession.setProxy({ mode: 'direct' as never });
console.log('[proxy] Electron session proxy: direct (no proxy)');
}
} catch (err) {
console.warn('[proxy] Failed to apply Electron session proxy:', err);
}
}

function createWindow(port: number) {
const windowOptions: Electron.BrowserWindowConstructorOptions = {
width: 1280,
Expand DownExpand Up@@ -319,6 +359,7 @@ app.whenReady().then(async () => {
}

serverPort = port;
await applyProxySettings(port);
createWindow(port);
} catch (err) {
console.error('Failed to start:', err);
Expand DownExpand Up@@ -349,6 +390,7 @@ app.on('activate', async () => {
await waitForServer(port);
serverPort = port;
}
await applyProxySettings(serverPort || 3000);
createWindow(serverPort || 3000);
} catch (err) {
console.error('Failed to restart server:', err);
Expand Down
100 changes: 100 additions & 0 deletions scripts/test-proxy.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env node
/**
* Minimal HTTP CONNECT proxy for testing the proxy feature.
*
* Usage:
* 1. node scripts/test-proxy.mjs (starts on port 9999)
* 2. In CodePilot Settings, set proxy to http://127.0.0.1:9999, enable it
* 3. Send a message in chat
* 4. Watch this terminal — you should see CONNECT requests to api.anthropic.com
*
* Press Ctrl+C to stop.
*/

import http from 'node:http';
import net from 'node:net';
import { URL } from 'node:url';

const PORT = parseInt(process.argv[2] || '9999', 10);
let requestCount = 0;

const server = http.createServer((req, res) => {
// Handle normal HTTP proxy requests (non-CONNECT)
requestCount++;
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] #${requestCount} HTTP ${req.method} ${req.url}`);

try {
const target = new URL(req.url);
const proxyReq = http.request(
{
hostname: target.hostname,
port: target.port || 80,
path: target.pathname + target.search,
method: req.method,
headers: req.headers,
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
}
);
proxyReq.on('error', (err) => {
console.log(` -> ERROR: ${err.message}`);
res.writeHead(502);
res.end('Bad Gateway');
});
req.pipe(proxyReq);
} catch (err) {
res.writeHead(400);
res.end('Bad Request');
}
});

// Handle HTTPS CONNECT tunneling
server.on('connect', (req, clientSocket, head) => {
requestCount++;
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] #${requestCount} CONNECT ${req.url}`);

const [hostname, port] = req.url.split(':');
const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
clientSocket.write(
'HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: codepilot-test-proxy\r\n' +
'\r\n'
);
serverSocket.write(head);
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
});

serverSocket.on('error', (err) => {
console.log(` -> CONNECT ERROR to ${req.url}: ${err.message}`);
clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
clientSocket.end();
});

clientSocket.on('error', () => serverSocket.destroy());
});

server.listen(PORT, '127.0.0.1', () => {
console.log('');
console.log('='.repeat(60));
console.log(' CodePilot Test Proxy');
console.log('='.repeat(60));
console.log(` Listening on: http://127.0.0.1:${PORT}`);
console.log('');
console.log(' Steps:');
console.log(` 1. Open CodePilot Settings`);
console.log(` 2. Enable proxy, set URL to http://127.0.0.1:${PORT}`);
console.log(` 3. Save, then send a chat message`);
console.log(` 4. Watch this terminal for CONNECT/HTTP requests`);
console.log('');
console.log(' Expected output:');
console.log(' CONNECT api.anthropic.com:443');
console.log('');
console.log(' Press Ctrl+C to stop');
console.log('='.repeat(60));
console.log('');
});
3 changes: 3 additions & 0 deletions src/app/api/settings/app/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,9 @@ import { getSetting, setSetting } from '@/lib/db';
const ALLOWED_KEYS = [
'anthropic_auth_token',
'anthropic_base_url',
'proxy_enabled',
'proxy_url',
'proxy_bypass',
];

export async function GET() {
Expand Down
131 changes: 131 additions & 0 deletions src/app/settings/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ import {
CodeIcon,
SlidersHorizontalIcon,
Loading02Icon,
GlobeIcon,
} from "@hugeicons/core-free-icons";

interface SettingsData {
Expand DownExpand Up@@ -165,6 +166,135 @@ function ApiConfigSection() {
);
}

// --- Proxy Configuration Section (CodePilot app settings, stored in SQLite) ---
function ProxyConfigSection() {
const [proxyEnabled, setProxyEnabled] = useState(false);
const [proxyUrl, setProxyUrl] = useState("");
const [proxyBypass, setProxyBypass] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [status, setStatus] = useState<"idle" | "saved" | "error">("idle");

useEffect(() => {
fetch("/api/settings/app")
.then((r) => r.json())
.then((data) => {
const s = data.settings || {};
setProxyEnabled(s.proxy_enabled === "true");
setProxyUrl(s.proxy_url || "");
setProxyBypass(s.proxy_bypass || "");
})
.catch(() => {})
.finally(() => setLoading(false));
}, []);

const handleSave = async () => {
setSaving(true);
setStatus("idle");
try {
const res = await fetch("/api/settings/app", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
settings: {
proxy_enabled: proxyEnabled ? "true" : "false",
proxy_url: proxyUrl,
proxy_bypass: proxyBypass,
},
}),
});
if (res.ok) {
setStatus("saved");
setTimeout(() => setStatus("idle"), 2000);
} else {
setStatus("error");
}
} catch {
setStatus("error");
} finally {
setSaving(false);
}
};

if (loading) return null;

return (
<div className="rounded-lg border border-border/50 p-4 space-y-4">
<div>
<div className="flex items-center gap-2">
<HugeiconsIcon icon={GlobeIcon} className="h-4 w-4 text-muted-foreground" />
<Label className="text-sm font-medium">Proxy Configuration</Label>
</div>
<p className="text-xs text-muted-foreground mt-1">
Configure an HTTP/HTTPS/SOCKS5 proxy for all network access. The proxy
environment variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY) will be
passed to the Claude Code subprocess.
</p>
</div>
<div className="flex items-center gap-3">
<Switch
id="proxy-enabled"
checked={proxyEnabled}
onCheckedChange={setProxyEnabled}
/>
<Label htmlFor="proxy-enabled" className="text-sm">
{proxyEnabled ? "Proxy Enabled" : "Proxy Disabled"}
</Label>
</div>
<div className="space-y-3">
<div>
<Label htmlFor="proxy-url" className="text-xs text-muted-foreground">
Proxy URL
</Label>
<Input
id="proxy-url"
placeholder="http://127.0.0.1:7890"
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
disabled={!proxyEnabled}
className="mt-1 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Supports http://, https://, socks5:// protocols
</p>
</div>
<div>
<Label htmlFor="proxy-bypass" className="text-xs text-muted-foreground">
Bypass List (NO_PROXY)
</Label>
<Input
id="proxy-bypass"
placeholder="localhost,127.0.0.1,::1"
value={proxyBypass}
onChange={(e) => setProxyBypass(e.target.value)}
disabled={!proxyEnabled}
className="mt-1 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Comma-separated list of hosts that should bypass the proxy
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Button onClick={handleSave} disabled={saving} size="sm" className="gap-2">
{saving ? (
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
) : (
<HugeiconsIcon icon={FloppyDiskIcon} className="h-4 w-4" />
)}
{saving ? "Saving..." : "Save Proxy Config"}
</Button>
{status === "saved" && (
<span className="text-sm text-green-600 dark:text-green-400">Saved</span>
)}
{status === "error" && (
<span className="text-sm text-destructive">Failed to save</span>
)}
</div>
</div>
);
}

// --- Claude CLI Settings Section (manages ~/.claude/settings.json) ---
function SettingsPageInner() {
const [settings, setSettings] = useState<SettingsData>({});
Expand DownExpand Up@@ -281,6 +411,7 @@ function SettingsPageInner() {
<div className="flex-1 overflow-auto p-6">
<div className="max-w-3xl space-y-6">
<ApiConfigSection />
<ProxyConfigSection />

{loading ? (
<div className="flex items-center justify-center py-12">
Expand Down
19 changes: 19 additions & 0 deletions src/lib/claude-client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,25 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
sdkEnv.ANTHROPIC_BASE_URL = appBaseUrl;
}

// Inject proxy environment variables if proxy is enabled
const proxyEnabled = getSetting('proxy_enabled');
if (proxyEnabled === 'true') {
const proxyUrl = getSetting('proxy_url');
const proxyBypass = getSetting('proxy_bypass');
if (proxyUrl) {
sdkEnv.HTTP_PROXY = proxyUrl;
sdkEnv.HTTPS_PROXY = proxyUrl;
sdkEnv.ALL_PROXY = proxyUrl;
sdkEnv.http_proxy = proxyUrl;
sdkEnv.https_proxy = proxyUrl;
sdkEnv.all_proxy = proxyUrl;
}
if (proxyBypass) {
sdkEnv.NO_PROXY = proxyBypass;
sdkEnv.no_proxy = proxyBypass;
}
}

const queryOptions: Options = {
cwd: workingDirectory || process.cwd(),
abortController,
Expand Down
3 changes: 3 additions & 0 deletions src/types/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -345,6 +345,9 @@ export const SETTING_KEYS = {
THEME: 'theme',
PERMISSION_MODE: 'permission_mode',
MAX_THINKING_TOKENS: 'max_thinking_tokens',
PROXY_ENABLED: 'proxy_enabled',
PROXY_URL: 'proxy_url',
PROXY_BYPASS: 'proxy_bypass',
} as const;

// ==========================================
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
44 changes: 43 additions & 1 deletion electron/main.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { app, BrowserWindow, nativeImage, dialog } from 'electron';
import { app, BrowserWindow, nativeImage, dialog, session } from 'electron';
import path from 'path';
import { spawn, execFileSync, ChildProcess } from 'child_process';
import fs from 'fs';
Expand DownExpand Up@@ -253,6 +253,46 @@ function getIconPath(): string {
return path.join(process.resourcesPath, 'icon.icns');
}

/**
* Fetch proxy settings from the CodePilot API and apply them
* to Electron's default session so renderer network requests
* (e.g. remote images in markdown) also route through the proxy.
*/
async function applyProxySettings(port: number): Promise<void> {
try {
const http = require('http');
const data: string = await new Promise((resolve, reject) => {
const req = http.get(`http://127.0.0.1:${port}/api/settings/app`, (res: { statusCode?: number; on: Function }) => {
let body = '';
res.on('data', (chunk: string) => { body += chunk; });
res.on('end', () => resolve(body));
});
req.on('error', reject);
req.setTimeout(3000, () => { req.destroy(); reject(new Error('timeout')); });
});

const parsed = JSON.parse(data);
const settings = parsed?.settings || {};

if (settings.proxy_enabled === 'true' && settings.proxy_url) {
const proxyUrl = settings.proxy_url;
const bypass = settings.proxy_bypass || 'localhost,127.0.0.1';
// Electron's setProxy expects proxyRules in the format: scheme://host:port
await session.defaultSession.setProxy({
proxyRules: proxyUrl,
proxyBypassRules: bypass,
});
console.log(`[proxy] Electron session proxy set to ${proxyUrl} (bypass: ${bypass})`);
} else {
// Ensure proxy is cleared if disabled
await session.defaultSession.setProxy({ mode: 'direct' as never });
console.log('[proxy] Electron session proxy: direct (no proxy)');
}
} catch (err) {
console.warn('[proxy] Failed to apply Electron session proxy:', err);
}
}

function createWindow(port: number) {
const windowOptions: Electron.BrowserWindowConstructorOptions = {
width: 1280,
Expand DownExpand Up@@ -319,6 +359,7 @@ app.whenReady().then(async () => {
}

serverPort = port;
await applyProxySettings(port);
createWindow(port);
} catch (err) {
console.error('Failed to start:', err);
Expand DownExpand Up@@ -349,6 +390,7 @@ app.on('activate', async () => {
await waitForServer(port);
serverPort = port;
}
await applyProxySettings(serverPort || 3000);
createWindow(serverPort || 3000);
} catch (err) {
console.error('Failed to restart server:', err);
Expand Down
100 changes: 100 additions & 0 deletions scripts/test-proxy.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env node
/**
* Minimal HTTP CONNECT proxy for testing the proxy feature.
*
* Usage:
* 1. node scripts/test-proxy.mjs (starts on port 9999)
* 2. In CodePilot Settings, set proxy to http://127.0.0.1:9999, enable it
* 3. Send a message in chat
* 4. Watch this terminal — you should see CONNECT requests to api.anthropic.com
*
* Press Ctrl+C to stop.
*/

import http from 'node:http';
import net from 'node:net';
import { URL } from 'node:url';

const PORT = parseInt(process.argv[2] || '9999', 10);
let requestCount = 0;

const server = http.createServer((req, res) => {
// Handle normal HTTP proxy requests (non-CONNECT)
requestCount++;
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] #${requestCount} HTTP ${req.method} ${req.url}`);

try {
const target = new URL(req.url);
const proxyReq = http.request(
{
hostname: target.hostname,
port: target.port || 80,
path: target.pathname + target.search,
method: req.method,
headers: req.headers,
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
}
);
proxyReq.on('error', (err) => {
console.log(` -> ERROR: ${err.message}`);
res.writeHead(502);
res.end('Bad Gateway');
});
req.pipe(proxyReq);
} catch (err) {
res.writeHead(400);
res.end('Bad Request');
}
});

// Handle HTTPS CONNECT tunneling
server.on('connect', (req, clientSocket, head) => {
requestCount++;
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] #${requestCount} CONNECT ${req.url}`);

const [hostname, port] = req.url.split(':');
const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
clientSocket.write(
'HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: codepilot-test-proxy\r\n' +
'\r\n'
);
serverSocket.write(head);
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
});

serverSocket.on('error', (err) => {
console.log(` -> CONNECT ERROR to ${req.url}: ${err.message}`);
clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
clientSocket.end();
});

clientSocket.on('error', () => serverSocket.destroy());
});

server.listen(PORT, '127.0.0.1', () => {
console.log('');
console.log('='.repeat(60));
console.log(' CodePilot Test Proxy');
console.log('='.repeat(60));
console.log(` Listening on: http://127.0.0.1:${PORT}`);
console.log('');
console.log(' Steps:');
console.log(` 1. Open CodePilot Settings`);
console.log(` 2. Enable proxy, set URL to http://127.0.0.1:${PORT}`);
console.log(` 3. Save, then send a chat message`);
console.log(` 4. Watch this terminal for CONNECT/HTTP requests`);
console.log('');
console.log(' Expected output:');
console.log(' CONNECT api.anthropic.com:443');
console.log('');
console.log(' Press Ctrl+C to stop');
console.log('='.repeat(60));
console.log('');
});
3 changes: 3 additions & 0 deletions src/app/api/settings/app/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,9 @@ import { getSetting, setSetting } from '@/lib/db';
const ALLOWED_KEYS = [
'anthropic_auth_token',
'anthropic_base_url',
'proxy_enabled',
'proxy_url',
'proxy_bypass',
];

export async function GET() {
Expand Down
131 changes: 131 additions & 0 deletions src/app/settings/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ import {
CodeIcon,
SlidersHorizontalIcon,
Loading02Icon,
GlobeIcon,
} from "@hugeicons/core-free-icons";

interface SettingsData {
Expand DownExpand Up@@ -165,6 +166,135 @@ function ApiConfigSection() {
);
}

// --- Proxy Configuration Section (CodePilot app settings, stored in SQLite) ---
function ProxyConfigSection() {
const [proxyEnabled, setProxyEnabled] = useState(false);
const [proxyUrl, setProxyUrl] = useState("");
const [proxyBypass, setProxyBypass] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [status, setStatus] = useState<"idle" | "saved" | "error">("idle");

useEffect(() => {
fetch("/api/settings/app")
.then((r) => r.json())
.then((data) => {
const s = data.settings || {};
setProxyEnabled(s.proxy_enabled === "true");
setProxyUrl(s.proxy_url || "");
setProxyBypass(s.proxy_bypass || "");
})
.catch(() => {})
.finally(() => setLoading(false));
}, []);

const handleSave = async () => {
setSaving(true);
setStatus("idle");
try {
const res = await fetch("/api/settings/app", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
settings: {
proxy_enabled: proxyEnabled ? "true" : "false",
proxy_url: proxyUrl,
proxy_bypass: proxyBypass,
},
}),
});
if (res.ok) {
setStatus("saved");
setTimeout(() => setStatus("idle"), 2000);
} else {
setStatus("error");
}
} catch {
setStatus("error");
} finally {
setSaving(false);
}
};

if (loading) return null;

return (
<div className="rounded-lg border border-border/50 p-4 space-y-4">
<div>
<div className="flex items-center gap-2">
<HugeiconsIcon icon={GlobeIcon} className="h-4 w-4 text-muted-foreground" />
<Label className="text-sm font-medium">Proxy Configuration</Label>
</div>
<p className="text-xs text-muted-foreground mt-1">
Configure an HTTP/HTTPS/SOCKS5 proxy for all network access. The proxy
environment variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY) will be
passed to the Claude Code subprocess.
</p>
</div>
<div className="flex items-center gap-3">
<Switch
id="proxy-enabled"
checked={proxyEnabled}
onCheckedChange={setProxyEnabled}
/>
<Label htmlFor="proxy-enabled" className="text-sm">
{proxyEnabled ? "Proxy Enabled" : "Proxy Disabled"}
</Label>
</div>
<div className="space-y-3">
<div>
<Label htmlFor="proxy-url" className="text-xs text-muted-foreground">
Proxy URL
</Label>
<Input
id="proxy-url"
placeholder="http://127.0.0.1:7890"
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
disabled={!proxyEnabled}
className="mt-1 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Supports http://, https://, socks5:// protocols
</p>
</div>
<div>
<Label htmlFor="proxy-bypass" className="text-xs text-muted-foreground">
Bypass List (NO_PROXY)
</Label>
<Input
id="proxy-bypass"
placeholder="localhost,127.0.0.1,::1"
value={proxyBypass}
onChange={(e) => setProxyBypass(e.target.value)}
disabled={!proxyEnabled}
className="mt-1 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Comma-separated list of hosts that should bypass the proxy
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Button onClick={handleSave} disabled={saving} size="sm" className="gap-2">
{saving ? (
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
) : (
<HugeiconsIcon icon={FloppyDiskIcon} className="h-4 w-4" />
)}
{saving ? "Saving..." : "Save Proxy Config"}
</Button>
{status === "saved" && (
<span className="text-sm text-green-600 dark:text-green-400">Saved</span>
)}
{status === "error" && (
<span className="text-sm text-destructive">Failed to save</span>
)}
</div>
</div>
);
}

// --- Claude CLI Settings Section (manages ~/.claude/settings.json) ---
function SettingsPageInner() {
const [settings, setSettings] = useState<SettingsData>({});
Expand DownExpand Up@@ -281,6 +411,7 @@ function SettingsPageInner() {
<div className="flex-1 overflow-auto p-6">
<div className="max-w-3xl space-y-6">
<ApiConfigSection />
<ProxyConfigSection />

{loading ? (
<div className="flex items-center justify-center py-12">
Expand Down
19 changes: 19 additions & 0 deletions src/lib/claude-client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,25 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
sdkEnv.ANTHROPIC_BASE_URL = appBaseUrl;
}

// Inject proxy environment variables if proxy is enabled
const proxyEnabled = getSetting('proxy_enabled');
if (proxyEnabled === 'true') {
const proxyUrl = getSetting('proxy_url');
const proxyBypass = getSetting('proxy_bypass');
if (proxyUrl) {
sdkEnv.HTTP_PROXY = proxyUrl;
sdkEnv.HTTPS_PROXY = proxyUrl;
sdkEnv.ALL_PROXY = proxyUrl;
sdkEnv.http_proxy = proxyUrl;
sdkEnv.https_proxy = proxyUrl;
sdkEnv.all_proxy = proxyUrl;
}
if (proxyBypass) {
sdkEnv.NO_PROXY = proxyBypass;
sdkEnv.no_proxy = proxyBypass;
}
}

const queryOptions: Options = {
cwd: workingDirectory || process.cwd(),
abortController,
Expand Down
3 changes: 3 additions & 0 deletions src/types/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -345,6 +345,9 @@ export const SETTING_KEYS = {
THEME: 'theme',
PERMISSION_MODE: 'permission_mode',
MAX_THINKING_TOKENS: 'max_thinking_tokens',
PROXY_ENABLED: 'proxy_enabled',
PROXY_URL: 'proxy_url',
PROXY_BYPASS: 'proxy_bypass',
} as const;

// ==========================================
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
44 changes: 43 additions & 1 deletion electron/main.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { app, BrowserWindow, nativeImage, dialog } from 'electron';
import { app, BrowserWindow, nativeImage, dialog, session } from 'electron';
import path from 'path';
import { spawn, execFileSync, ChildProcess } from 'child_process';
import fs from 'fs';
Expand DownExpand Up@@ -253,6 +253,46 @@ function getIconPath(): string {
return path.join(process.resourcesPath, 'icon.icns');
}

/**
* Fetch proxy settings from the CodePilot API and apply them
* to Electron's default session so renderer network requests
* (e.g. remote images in markdown) also route through the proxy.
*/
async function applyProxySettings(port: number): Promise<void> {
try {
const http = require('http');
const data: string = await new Promise((resolve, reject) => {
const req = http.get(`http://127.0.0.1:${port}/api/settings/app`, (res: { statusCode?: number; on: Function }) => {
let body = '';
res.on('data', (chunk: string) => { body += chunk; });
res.on('end', () => resolve(body));
});
req.on('error', reject);
req.setTimeout(3000, () => { req.destroy(); reject(new Error('timeout')); });
});

const parsed = JSON.parse(data);
const settings = parsed?.settings || {};

if (settings.proxy_enabled === 'true' && settings.proxy_url) {
const proxyUrl = settings.proxy_url;
const bypass = settings.proxy_bypass || 'localhost,127.0.0.1';
// Electron's setProxy expects proxyRules in the format: scheme://host:port
await session.defaultSession.setProxy({
proxyRules: proxyUrl,
proxyBypassRules: bypass,
});
console.log(`[proxy] Electron session proxy set to ${proxyUrl} (bypass: ${bypass})`);
} else {
// Ensure proxy is cleared if disabled
await session.defaultSession.setProxy({ mode: 'direct' as never });
console.log('[proxy] Electron session proxy: direct (no proxy)');
}
} catch (err) {
console.warn('[proxy] Failed to apply Electron session proxy:', err);
}
}

function createWindow(port: number) {
const windowOptions: Electron.BrowserWindowConstructorOptions = {
width: 1280,
Expand DownExpand Up@@ -319,6 +359,7 @@ app.whenReady().then(async () => {
}

serverPort = port;
await applyProxySettings(port);
createWindow(port);
} catch (err) {
console.error('Failed to start:', err);
Expand DownExpand Up@@ -349,6 +390,7 @@ app.on('activate', async () => {
await waitForServer(port);
serverPort = port;
}
await applyProxySettings(serverPort || 3000);
createWindow(serverPort || 3000);
} catch (err) {
console.error('Failed to restart server:', err);
Expand Down
100 changes: 100 additions & 0 deletions scripts/test-proxy.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env node
/**
* Minimal HTTP CONNECT proxy for testing the proxy feature.
*
* Usage:
* 1. node scripts/test-proxy.mjs (starts on port 9999)
* 2. In CodePilot Settings, set proxy to http://127.0.0.1:9999, enable it
* 3. Send a message in chat
* 4. Watch this terminal — you should see CONNECT requests to api.anthropic.com
*
* Press Ctrl+C to stop.
*/

import http from 'node:http';
import net from 'node:net';
import { URL } from 'node:url';

const PORT = parseInt(process.argv[2] || '9999', 10);
let requestCount = 0;

const server = http.createServer((req, res) => {
// Handle normal HTTP proxy requests (non-CONNECT)
requestCount++;
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] #${requestCount} HTTP ${req.method} ${req.url}`);

try {
const target = new URL(req.url);
const proxyReq = http.request(
{
hostname: target.hostname,
port: target.port || 80,
path: target.pathname + target.search,
method: req.method,
headers: req.headers,
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
}
);
proxyReq.on('error', (err) => {
console.log(` -> ERROR: ${err.message}`);
res.writeHead(502);
res.end('Bad Gateway');
});
req.pipe(proxyReq);
} catch (err) {
res.writeHead(400);
res.end('Bad Request');
}
});

// Handle HTTPS CONNECT tunneling
server.on('connect', (req, clientSocket, head) => {
requestCount++;
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] #${requestCount} CONNECT ${req.url}`);

const [hostname, port] = req.url.split(':');
const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
clientSocket.write(
'HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: codepilot-test-proxy\r\n' +
'\r\n'
);
serverSocket.write(head);
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
});

serverSocket.on('error', (err) => {
console.log(` -> CONNECT ERROR to ${req.url}: ${err.message}`);
clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
clientSocket.end();
});

clientSocket.on('error', () => serverSocket.destroy());
});

server.listen(PORT, '127.0.0.1', () => {
console.log('');
console.log('='.repeat(60));
console.log(' CodePilot Test Proxy');
console.log('='.repeat(60));
console.log(` Listening on: http://127.0.0.1:${PORT}`);
console.log('');
console.log(' Steps:');
console.log(` 1. Open CodePilot Settings`);
console.log(` 2. Enable proxy, set URL to http://127.0.0.1:${PORT}`);
console.log(` 3. Save, then send a chat message`);
console.log(` 4. Watch this terminal for CONNECT/HTTP requests`);
console.log('');
console.log(' Expected output:');
console.log(' CONNECT api.anthropic.com:443');
console.log('');
console.log(' Press Ctrl+C to stop');
console.log('='.repeat(60));
console.log('');
});
3 changes: 3 additions & 0 deletions src/app/api/settings/app/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,9 @@ import { getSetting, setSetting } from '@/lib/db';
const ALLOWED_KEYS = [
'anthropic_auth_token',
'anthropic_base_url',
'proxy_enabled',
'proxy_url',
'proxy_bypass',
];

export async function GET() {
Expand Down
131 changes: 131 additions & 0 deletions src/app/settings/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ import {
CodeIcon,
SlidersHorizontalIcon,
Loading02Icon,
GlobeIcon,
} from "@hugeicons/core-free-icons";

interface SettingsData {
Expand DownExpand Up@@ -165,6 +166,135 @@ function ApiConfigSection() {
);
}

// --- Proxy Configuration Section (CodePilot app settings, stored in SQLite) ---
function ProxyConfigSection() {
const [proxyEnabled, setProxyEnabled] = useState(false);
const [proxyUrl, setProxyUrl] = useState("");
const [proxyBypass, setProxyBypass] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [status, setStatus] = useState<"idle" | "saved" | "error">("idle");

useEffect(() => {
fetch("/api/settings/app")
.then((r) => r.json())
.then((data) => {
const s = data.settings || {};
setProxyEnabled(s.proxy_enabled === "true");
setProxyUrl(s.proxy_url || "");
setProxyBypass(s.proxy_bypass || "");
})
.catch(() => {})
.finally(() => setLoading(false));
}, []);

const handleSave = async () => {
setSaving(true);
setStatus("idle");
try {
const res = await fetch("/api/settings/app", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
settings: {
proxy_enabled: proxyEnabled ? "true" : "false",
proxy_url: proxyUrl,
proxy_bypass: proxyBypass,
},
}),
});
if (res.ok) {
setStatus("saved");
setTimeout(() => setStatus("idle"), 2000);
} else {
setStatus("error");
}
} catch {
setStatus("error");
} finally {
setSaving(false);
}
};

if (loading) return null;

return (
<div className="rounded-lg border border-border/50 p-4 space-y-4">
<div>
<div className="flex items-center gap-2">
<HugeiconsIcon icon={GlobeIcon} className="h-4 w-4 text-muted-foreground" />
<Label className="text-sm font-medium">Proxy Configuration</Label>
</div>
<p className="text-xs text-muted-foreground mt-1">
Configure an HTTP/HTTPS/SOCKS5 proxy for all network access. The proxy
environment variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY) will be
passed to the Claude Code subprocess.
</p>
</div>
<div className="flex items-center gap-3">
<Switch
id="proxy-enabled"
checked={proxyEnabled}
onCheckedChange={setProxyEnabled}
/>
<Label htmlFor="proxy-enabled" className="text-sm">
{proxyEnabled ? "Proxy Enabled" : "Proxy Disabled"}
</Label>
</div>
<div className="space-y-3">
<div>
<Label htmlFor="proxy-url" className="text-xs text-muted-foreground">
Proxy URL
</Label>
<Input
id="proxy-url"
placeholder="http://127.0.0.1:7890"
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
disabled={!proxyEnabled}
className="mt-1 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Supports http://, https://, socks5:// protocols
</p>
</div>
<div>
<Label htmlFor="proxy-bypass" className="text-xs text-muted-foreground">
Bypass List (NO_PROXY)
</Label>
<Input
id="proxy-bypass"
placeholder="localhost,127.0.0.1,::1"
value={proxyBypass}
onChange={(e) => setProxyBypass(e.target.value)}
disabled={!proxyEnabled}
className="mt-1 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Comma-separated list of hosts that should bypass the proxy
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Button onClick={handleSave} disabled={saving} size="sm" className="gap-2">
{saving ? (
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
) : (
<HugeiconsIcon icon={FloppyDiskIcon} className="h-4 w-4" />
)}
{saving ? "Saving..." : "Save Proxy Config"}
</Button>
{status === "saved" && (
<span className="text-sm text-green-600 dark:text-green-400">Saved</span>
)}
{status === "error" && (
<span className="text-sm text-destructive">Failed to save</span>
)}
</div>
</div>
);
}

// --- Claude CLI Settings Section (manages ~/.claude/settings.json) ---
function SettingsPageInner() {
const [settings, setSettings] = useState<SettingsData>({});
Expand DownExpand Up@@ -281,6 +411,7 @@ function SettingsPageInner() {
<div className="flex-1 overflow-auto p-6">
<div className="max-w-3xl space-y-6">
<ApiConfigSection />
<ProxyConfigSection />

{loading ? (
<div className="flex items-center justify-center py-12">
Expand Down
19 changes: 19 additions & 0 deletions src/lib/claude-client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,25 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
sdkEnv.ANTHROPIC_BASE_URL = appBaseUrl;
}

// Inject proxy environment variables if proxy is enabled
const proxyEnabled = getSetting('proxy_enabled');
if (proxyEnabled === 'true') {
const proxyUrl = getSetting('proxy_url');
const proxyBypass = getSetting('proxy_bypass');
if (proxyUrl) {
sdkEnv.HTTP_PROXY = proxyUrl;
sdkEnv.HTTPS_PROXY = proxyUrl;
sdkEnv.ALL_PROXY = proxyUrl;
sdkEnv.http_proxy = proxyUrl;
sdkEnv.https_proxy = proxyUrl;
sdkEnv.all_proxy = proxyUrl;
}
if (proxyBypass) {
sdkEnv.NO_PROXY = proxyBypass;
sdkEnv.no_proxy = proxyBypass;
}
}

const queryOptions: Options = {
cwd: workingDirectory || process.cwd(),
abortController,
Expand Down
3 changes: 3 additions & 0 deletions src/types/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -345,6 +345,9 @@ export const SETTING_KEYS = {
THEME: 'theme',
PERMISSION_MODE: 'permission_mode',
MAX_THINKING_TOKENS: 'max_thinking_tokens',
PROXY_ENABLED: 'proxy_enabled',
PROXY_URL: 'proxy_url',
PROXY_BYPASS: 'proxy_bypass',
} as const;

// ==========================================
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
44 changes: 43 additions & 1 deletion electron/main.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { app, BrowserWindow, nativeImage, dialog } from 'electron';
import { app, BrowserWindow, nativeImage, dialog, session } from 'electron';
import path from 'path';
import { spawn, execFileSync, ChildProcess } from 'child_process';
import fs from 'fs';
Expand DownExpand Up@@ -253,6 +253,46 @@ function getIconPath(): string {
return path.join(process.resourcesPath, 'icon.icns');
}

/**
* Fetch proxy settings from the CodePilot API and apply them
* to Electron's default session so renderer network requests
* (e.g. remote images in markdown) also route through the proxy.
*/
async function applyProxySettings(port: number): Promise<void> {
try {
const http = require('http');
const data: string = await new Promise((resolve, reject) => {
const req = http.get(`http://127.0.0.1:${port}/api/settings/app`, (res: { statusCode?: number; on: Function }) => {
let body = '';
res.on('data', (chunk: string) => { body += chunk; });
res.on('end', () => resolve(body));
});
req.on('error', reject);
req.setTimeout(3000, () => { req.destroy(); reject(new Error('timeout')); });
});

const parsed = JSON.parse(data);
const settings = parsed?.settings || {};

if (settings.proxy_enabled === 'true' && settings.proxy_url) {
const proxyUrl = settings.proxy_url;
const bypass = settings.proxy_bypass || 'localhost,127.0.0.1';
// Electron's setProxy expects proxyRules in the format: scheme://host:port
await session.defaultSession.setProxy({
proxyRules: proxyUrl,
proxyBypassRules: bypass,
});
console.log(`[proxy] Electron session proxy set to ${proxyUrl} (bypass: ${bypass})`);
} else {
// Ensure proxy is cleared if disabled
await session.defaultSession.setProxy({ mode: 'direct' as never });
console.log('[proxy] Electron session proxy: direct (no proxy)');
}
} catch (err) {
console.warn('[proxy] Failed to apply Electron session proxy:', err);
}
}

function createWindow(port: number) {
const windowOptions: Electron.BrowserWindowConstructorOptions = {
width: 1280,
Expand DownExpand Up@@ -319,6 +359,7 @@ app.whenReady().then(async () => {
}

serverPort = port;
await applyProxySettings(port);
createWindow(port);
} catch (err) {
console.error('Failed to start:', err);
Expand DownExpand Up@@ -349,6 +390,7 @@ app.on('activate', async () => {
await waitForServer(port);
serverPort = port;
}
await applyProxySettings(serverPort || 3000);
createWindow(serverPort || 3000);
} catch (err) {
console.error('Failed to restart server:', err);
Expand Down
100 changes: 100 additions & 0 deletions scripts/test-proxy.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env node
/**
* Minimal HTTP CONNECT proxy for testing the proxy feature.
*
* Usage:
* 1. node scripts/test-proxy.mjs (starts on port 9999)
* 2. In CodePilot Settings, set proxy to http://127.0.0.1:9999, enable it
* 3. Send a message in chat
* 4. Watch this terminal — you should see CONNECT requests to api.anthropic.com
*
* Press Ctrl+C to stop.
*/

import http from 'node:http';
import net from 'node:net';
import { URL } from 'node:url';

const PORT = parseInt(process.argv[2] || '9999', 10);
let requestCount = 0;

const server = http.createServer((req, res) => {
// Handle normal HTTP proxy requests (non-CONNECT)
requestCount++;
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] #${requestCount} HTTP ${req.method} ${req.url}`);

try {
const target = new URL(req.url);
const proxyReq = http.request(
{
hostname: target.hostname,
port: target.port || 80,
path: target.pathname + target.search,
method: req.method,
headers: req.headers,
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
}
);
proxyReq.on('error', (err) => {
console.log(` -> ERROR: ${err.message}`);
res.writeHead(502);
res.end('Bad Gateway');
});
req.pipe(proxyReq);
} catch (err) {
res.writeHead(400);
res.end('Bad Request');
}
});

// Handle HTTPS CONNECT tunneling
server.on('connect', (req, clientSocket, head) => {
requestCount++;
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] #${requestCount} CONNECT ${req.url}`);

const [hostname, port] = req.url.split(':');
const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
clientSocket.write(
'HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: codepilot-test-proxy\r\n' +
'\r\n'
);
serverSocket.write(head);
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
});

serverSocket.on('error', (err) => {
console.log(` -> CONNECT ERROR to ${req.url}: ${err.message}`);
clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
clientSocket.end();
});

clientSocket.on('error', () => serverSocket.destroy());
});

server.listen(PORT, '127.0.0.1', () => {
console.log('');
console.log('='.repeat(60));
console.log(' CodePilot Test Proxy');
console.log('='.repeat(60));
console.log(` Listening on: http://127.0.0.1:${PORT}`);
console.log('');
console.log(' Steps:');
console.log(` 1. Open CodePilot Settings`);
console.log(` 2. Enable proxy, set URL to http://127.0.0.1:${PORT}`);
console.log(` 3. Save, then send a chat message`);
console.log(` 4. Watch this terminal for CONNECT/HTTP requests`);
console.log('');
console.log(' Expected output:');
console.log(' CONNECT api.anthropic.com:443');
console.log('');
console.log(' Press Ctrl+C to stop');
console.log('='.repeat(60));
console.log('');
});
3 changes: 3 additions & 0 deletions src/app/api/settings/app/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,9 @@ import { getSetting, setSetting } from '@/lib/db';
const ALLOWED_KEYS = [
'anthropic_auth_token',
'anthropic_base_url',
'proxy_enabled',
'proxy_url',
'proxy_bypass',
];

export async function GET() {
Expand Down
131 changes: 131 additions & 0 deletions src/app/settings/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ import {
CodeIcon,
SlidersHorizontalIcon,
Loading02Icon,
GlobeIcon,
} from "@hugeicons/core-free-icons";

interface SettingsData {
Expand DownExpand Up@@ -165,6 +166,135 @@ function ApiConfigSection() {
);
}

// --- Proxy Configuration Section (CodePilot app settings, stored in SQLite) ---
function ProxyConfigSection() {
const [proxyEnabled, setProxyEnabled] = useState(false);
const [proxyUrl, setProxyUrl] = useState("");
const [proxyBypass, setProxyBypass] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [status, setStatus] = useState<"idle" | "saved" | "error">("idle");

useEffect(() => {
fetch("/api/settings/app")
.then((r) => r.json())
.then((data) => {
const s = data.settings || {};
setProxyEnabled(s.proxy_enabled === "true");
setProxyUrl(s.proxy_url || "");
setProxyBypass(s.proxy_bypass || "");
})
.catch(() => {})
.finally(() => setLoading(false));
}, []);

const handleSave = async () => {
setSaving(true);
setStatus("idle");
try {
const res = await fetch("/api/settings/app", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
settings: {
proxy_enabled: proxyEnabled ? "true" : "false",
proxy_url: proxyUrl,
proxy_bypass: proxyBypass,
},
}),
});
if (res.ok) {
setStatus("saved");
setTimeout(() => setStatus("idle"), 2000);
} else {
setStatus("error");
}
} catch {
setStatus("error");
} finally {
setSaving(false);
}
};

if (loading) return null;

return (
<div className="rounded-lg border border-border/50 p-4 space-y-4">
<div>
<div className="flex items-center gap-2">
<HugeiconsIcon icon={GlobeIcon} className="h-4 w-4 text-muted-foreground" />
<Label className="text-sm font-medium">Proxy Configuration</Label>
</div>
<p className="text-xs text-muted-foreground mt-1">
Configure an HTTP/HTTPS/SOCKS5 proxy for all network access. The proxy
environment variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY) will be
passed to the Claude Code subprocess.
</p>
</div>
<div className="flex items-center gap-3">
<Switch
id="proxy-enabled"
checked={proxyEnabled}
onCheckedChange={setProxyEnabled}
/>
<Label htmlFor="proxy-enabled" className="text-sm">
{proxyEnabled ? "Proxy Enabled" : "Proxy Disabled"}
</Label>
</div>
<div className="space-y-3">
<div>
<Label htmlFor="proxy-url" className="text-xs text-muted-foreground">
Proxy URL
</Label>
<Input
id="proxy-url"
placeholder="http://127.0.0.1:7890"
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
disabled={!proxyEnabled}
className="mt-1 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Supports http://, https://, socks5:// protocols
</p>
</div>
<div>
<Label htmlFor="proxy-bypass" className="text-xs text-muted-foreground">
Bypass List (NO_PROXY)
</Label>
<Input
id="proxy-bypass"
placeholder="localhost,127.0.0.1,::1"
value={proxyBypass}
onChange={(e) => setProxyBypass(e.target.value)}
disabled={!proxyEnabled}
className="mt-1 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Comma-separated list of hosts that should bypass the proxy
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Button onClick={handleSave} disabled={saving} size="sm" className="gap-2">
{saving ? (
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
) : (
<HugeiconsIcon icon={FloppyDiskIcon} className="h-4 w-4" />
)}
{saving ? "Saving..." : "Save Proxy Config"}
</Button>
{status === "saved" && (
<span className="text-sm text-green-600 dark:text-green-400">Saved</span>
)}
{status === "error" && (
<span className="text-sm text-destructive">Failed to save</span>
)}
</div>
</div>
);
}

// --- Claude CLI Settings Section (manages ~/.claude/settings.json) ---
function SettingsPageInner() {
const [settings, setSettings] = useState<SettingsData>({});
Expand DownExpand Up@@ -281,6 +411,7 @@ function SettingsPageInner() {
<div className="flex-1 overflow-auto p-6">
<div className="max-w-3xl space-y-6">
<ApiConfigSection />
<ProxyConfigSection />

{loading ? (
<div className="flex items-center justify-center py-12">
Expand Down
19 changes: 19 additions & 0 deletions src/lib/claude-client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,25 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
sdkEnv.ANTHROPIC_BASE_URL = appBaseUrl;
}

// Inject proxy environment variables if proxy is enabled
const proxyEnabled = getSetting('proxy_enabled');
if (proxyEnabled === 'true') {
const proxyUrl = getSetting('proxy_url');
const proxyBypass = getSetting('proxy_bypass');
if (proxyUrl) {
sdkEnv.HTTP_PROXY = proxyUrl;
sdkEnv.HTTPS_PROXY = proxyUrl;
sdkEnv.ALL_PROXY = proxyUrl;
sdkEnv.http_proxy = proxyUrl;
sdkEnv.https_proxy = proxyUrl;
sdkEnv.all_proxy = proxyUrl;
}
if (proxyBypass) {
sdkEnv.NO_PROXY = proxyBypass;
sdkEnv.no_proxy = proxyBypass;
}
}

const queryOptions: Options = {
cwd: workingDirectory || process.cwd(),
abortController,
Expand Down
3 changes: 3 additions & 0 deletions src/types/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -345,6 +345,9 @@ export const SETTING_KEYS = {
THEME: 'theme',
PERMISSION_MODE: 'permission_mode',
MAX_THINKING_TOKENS: 'max_thinking_tokens',
PROXY_ENABLED: 'proxy_enabled',
PROXY_URL: 'proxy_url',
PROXY_BYPASS: 'proxy_bypass',
} as const;

// ==========================================
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
44 changes: 43 additions & 1 deletion electron/main.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { app, BrowserWindow, nativeImage, dialog } from 'electron';
import { app, BrowserWindow, nativeImage, dialog, session } from 'electron';
import path from 'path';
import { spawn, execFileSync, ChildProcess } from 'child_process';
import fs from 'fs';
Expand DownExpand Up@@ -253,6 +253,46 @@ function getIconPath(): string {
return path.join(process.resourcesPath, 'icon.icns');
}

/**
* Fetch proxy settings from the CodePilot API and apply them
* to Electron's default session so renderer network requests
* (e.g. remote images in markdown) also route through the proxy.
*/
async function applyProxySettings(port: number): Promise<void> {
try {
const http = require('http');
const data: string = await new Promise((resolve, reject) => {
const req = http.get(`http://127.0.0.1:${port}/api/settings/app`, (res: { statusCode?: number; on: Function }) => {
let body = '';
res.on('data', (chunk: string) => { body += chunk; });
res.on('end', () => resolve(body));
});
req.on('error', reject);
req.setTimeout(3000, () => { req.destroy(); reject(new Error('timeout')); });
});

const parsed = JSON.parse(data);
const settings = parsed?.settings || {};

if (settings.proxy_enabled === 'true' && settings.proxy_url) {
const proxyUrl = settings.proxy_url;
const bypass = settings.proxy_bypass || 'localhost,127.0.0.1';
// Electron's setProxy expects proxyRules in the format: scheme://host:port
await session.defaultSession.setProxy({
proxyRules: proxyUrl,
proxyBypassRules: bypass,
});
console.log(`[proxy] Electron session proxy set to ${proxyUrl} (bypass: ${bypass})`);
} else {
// Ensure proxy is cleared if disabled
await session.defaultSession.setProxy({ mode: 'direct' as never });
console.log('[proxy] Electron session proxy: direct (no proxy)');
}
} catch (err) {
console.warn('[proxy] Failed to apply Electron session proxy:', err);
}
}

function createWindow(port: number) {
const windowOptions: Electron.BrowserWindowConstructorOptions = {
width: 1280,
Expand DownExpand Up@@ -319,6 +359,7 @@ app.whenReady().then(async () => {
}

serverPort = port;
await applyProxySettings(port);
createWindow(port);
} catch (err) {
console.error('Failed to start:', err);
Expand DownExpand Up@@ -349,6 +390,7 @@ app.on('activate', async () => {
await waitForServer(port);
serverPort = port;
}
await applyProxySettings(serverPort || 3000);
createWindow(serverPort || 3000);
} catch (err) {
console.error('Failed to restart server:', err);
Expand Down
100 changes: 100 additions & 0 deletions scripts/test-proxy.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env node
/**
* Minimal HTTP CONNECT proxy for testing the proxy feature.
*
* Usage:
* 1. node scripts/test-proxy.mjs (starts on port 9999)
* 2. In CodePilot Settings, set proxy to http://127.0.0.1:9999, enable it
* 3. Send a message in chat
* 4. Watch this terminal — you should see CONNECT requests to api.anthropic.com
*
* Press Ctrl+C to stop.
*/

import http from 'node:http';
import net from 'node:net';
import { URL } from 'node:url';

const PORT = parseInt(process.argv[2] || '9999', 10);
let requestCount = 0;

const server = http.createServer((req, res) => {
// Handle normal HTTP proxy requests (non-CONNECT)
requestCount++;
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] #${requestCount} HTTP ${req.method} ${req.url}`);

try {
const target = new URL(req.url);
const proxyReq = http.request(
{
hostname: target.hostname,
port: target.port || 80,
path: target.pathname + target.search,
method: req.method,
headers: req.headers,
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
}
);
proxyReq.on('error', (err) => {
console.log(` -> ERROR: ${err.message}`);
res.writeHead(502);
res.end('Bad Gateway');
});
req.pipe(proxyReq);
} catch (err) {
res.writeHead(400);
res.end('Bad Request');
}
});

// Handle HTTPS CONNECT tunneling
server.on('connect', (req, clientSocket, head) => {
requestCount++;
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] #${requestCount} CONNECT ${req.url}`);

const [hostname, port] = req.url.split(':');
const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
clientSocket.write(
'HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: codepilot-test-proxy\r\n' +
'\r\n'
);
serverSocket.write(head);
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
});

serverSocket.on('error', (err) => {
console.log(` -> CONNECT ERROR to ${req.url}: ${err.message}`);
clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
clientSocket.end();
});

clientSocket.on('error', () => serverSocket.destroy());
});

server.listen(PORT, '127.0.0.1', () => {
console.log('');
console.log('='.repeat(60));
console.log(' CodePilot Test Proxy');
console.log('='.repeat(60));
console.log(` Listening on: http://127.0.0.1:${PORT}`);
console.log('');
console.log(' Steps:');
console.log(` 1. Open CodePilot Settings`);
console.log(` 2. Enable proxy, set URL to http://127.0.0.1:${PORT}`);
console.log(` 3. Save, then send a chat message`);
console.log(` 4. Watch this terminal for CONNECT/HTTP requests`);
console.log('');
console.log(' Expected output:');
console.log(' CONNECT api.anthropic.com:443');
console.log('');
console.log(' Press Ctrl+C to stop');
console.log('='.repeat(60));
console.log('');
});
3 changes: 3 additions & 0 deletions src/app/api/settings/app/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,9 @@ import { getSetting, setSetting } from '@/lib/db';
const ALLOWED_KEYS = [
'anthropic_auth_token',
'anthropic_base_url',
'proxy_enabled',
'proxy_url',
'proxy_bypass',
];

export async function GET() {
Expand Down
131 changes: 131 additions & 0 deletions src/app/settings/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ import {
CodeIcon,
SlidersHorizontalIcon,
Loading02Icon,
GlobeIcon,
} from "@hugeicons/core-free-icons";

interface SettingsData {
Expand DownExpand Up@@ -165,6 +166,135 @@ function ApiConfigSection() {
);
}

// --- Proxy Configuration Section (CodePilot app settings, stored in SQLite) ---
function ProxyConfigSection() {
const [proxyEnabled, setProxyEnabled] = useState(false);
const [proxyUrl, setProxyUrl] = useState("");
const [proxyBypass, setProxyBypass] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [status, setStatus] = useState<"idle" | "saved" | "error">("idle");

useEffect(() => {
fetch("/api/settings/app")
.then((r) => r.json())
.then((data) => {
const s = data.settings || {};
setProxyEnabled(s.proxy_enabled === "true");
setProxyUrl(s.proxy_url || "");
setProxyBypass(s.proxy_bypass || "");
})
.catch(() => {})
.finally(() => setLoading(false));
}, []);

const handleSave = async () => {
setSaving(true);
setStatus("idle");
try {
const res = await fetch("/api/settings/app", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
settings: {
proxy_enabled: proxyEnabled ? "true" : "false",
proxy_url: proxyUrl,
proxy_bypass: proxyBypass,
},
}),
});
if (res.ok) {
setStatus("saved");
setTimeout(() => setStatus("idle"), 2000);
} else {
setStatus("error");
}
} catch {
setStatus("error");
} finally {
setSaving(false);
}
};

if (loading) return null;

return (
<div className="rounded-lg border border-border/50 p-4 space-y-4">
<div>
<div className="flex items-center gap-2">
<HugeiconsIcon icon={GlobeIcon} className="h-4 w-4 text-muted-foreground" />
<Label className="text-sm font-medium">Proxy Configuration</Label>
</div>
<p className="text-xs text-muted-foreground mt-1">
Configure an HTTP/HTTPS/SOCKS5 proxy for all network access. The proxy
environment variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY) will be
passed to the Claude Code subprocess.
</p>
</div>
<div className="flex items-center gap-3">
<Switch
id="proxy-enabled"
checked={proxyEnabled}
onCheckedChange={setProxyEnabled}
/>
<Label htmlFor="proxy-enabled" className="text-sm">
{proxyEnabled ? "Proxy Enabled" : "Proxy Disabled"}
</Label>
</div>
<div className="space-y-3">
<div>
<Label htmlFor="proxy-url" className="text-xs text-muted-foreground">
Proxy URL
</Label>
<Input
id="proxy-url"
placeholder="http://127.0.0.1:7890"
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
disabled={!proxyEnabled}
className="mt-1 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Supports http://, https://, socks5:// protocols
</p>
</div>
<div>
<Label htmlFor="proxy-bypass" className="text-xs text-muted-foreground">
Bypass List (NO_PROXY)
</Label>
<Input
id="proxy-bypass"
placeholder="localhost,127.0.0.1,::1"
value={proxyBypass}
onChange={(e) => setProxyBypass(e.target.value)}
disabled={!proxyEnabled}
className="mt-1 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Comma-separated list of hosts that should bypass the proxy
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Button onClick={handleSave} disabled={saving} size="sm" className="gap-2">
{saving ? (
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
) : (
<HugeiconsIcon icon={FloppyDiskIcon} className="h-4 w-4" />
)}
{saving ? "Saving..." : "Save Proxy Config"}
</Button>
{status === "saved" && (
<span className="text-sm text-green-600 dark:text-green-400">Saved</span>
)}
{status === "error" && (
<span className="text-sm text-destructive">Failed to save</span>
)}
</div>
</div>
);
}

// --- Claude CLI Settings Section (manages ~/.claude/settings.json) ---
function SettingsPageInner() {
const [settings, setSettings] = useState<SettingsData>({});
Expand DownExpand Up@@ -281,6 +411,7 @@ function SettingsPageInner() {
<div className="flex-1 overflow-auto p-6">
<div className="max-w-3xl space-y-6">
<ApiConfigSection />
<ProxyConfigSection />

{loading ? (
<div className="flex items-center justify-center py-12">
Expand Down
19 changes: 19 additions & 0 deletions src/lib/claude-client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,25 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
sdkEnv.ANTHROPIC_BASE_URL = appBaseUrl;
}

// Inject proxy environment variables if proxy is enabled
const proxyEnabled = getSetting('proxy_enabled');
if (proxyEnabled === 'true') {
const proxyUrl = getSetting('proxy_url');
const proxyBypass = getSetting('proxy_bypass');
if (proxyUrl) {
sdkEnv.HTTP_PROXY = proxyUrl;
sdkEnv.HTTPS_PROXY = proxyUrl;
sdkEnv.ALL_PROXY = proxyUrl;
sdkEnv.http_proxy = proxyUrl;
sdkEnv.https_proxy = proxyUrl;
sdkEnv.all_proxy = proxyUrl;
}
if (proxyBypass) {
sdkEnv.NO_PROXY = proxyBypass;
sdkEnv.no_proxy = proxyBypass;
}
}

const queryOptions: Options = {
cwd: workingDirectory || process.cwd(),
abortController,
Expand Down
3 changes: 3 additions & 0 deletions src/types/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -345,6 +345,9 @@ export const SETTING_KEYS = {
THEME: 'theme',
PERMISSION_MODE: 'permission_mode',
MAX_THINKING_TOKENS: 'max_thinking_tokens',
PROXY_ENABLED: 'proxy_enabled',
PROXY_URL: 'proxy_url',
PROXY_BYPASS: 'proxy_bypass',
} as const;

// ==========================================
Expand Down