+ {!isRunning && (
+
+
+
+ Start the proxy above before connecting a client. You can prepare the remaining setup
+ now.
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ OpenCode
+ cURL
+ Python
+
+
+
+
+
+ In Maple, create a dedicated API key above and copy it. The full key is shown only
+ once. Then return to Local Proxy.
+
+
+ In OpenCode, open Settings → Providers → Custom Provider → Connect.
+
+ Enter the values below, then connect and select the model.
+
+
+ {hasApiKeys && (
+
+ Existing keys cannot be revealed again. If you did not save one, create a new key for
+ OpenCode.
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Use a real Maple key—never a placeholder. An incoming bearer key overrides the key
+ saved by Maple Desktop.
+
+
+ When replacing an old key, disconnect and reconnect the provider. OpenCode stores
+ credentials separately from project configuration.
+
+
+
+
+
+
+
+ Set MAPLE_API_KEY to a real key created in Maple, then run this request.
+
+ {selectedModel ? (
+
+ ) : (
+
+ Load and select a current model before copying this example.
+
+ )}
+
+
+
+
+ Install the OpenAI Python package and set MAPLE_API_KEY to a real Maple
+ key.
+
+ {selectedModel ? (
+
+ ) : (
+
+ Load and select a current model before copying this example.
+
+ )}
+
+
+
+
+
+
+
+ A 401 usually means an invalid or stale key; a 400 usually means an old model ID. A 5xx
+ can be a temporary model-provider outage even when this setup is correct.
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/apikeys/ProxyConfigSection.tsx b/frontend/src/components/apikeys/ProxyConfigSection.tsx
index f602ea2b..ca2192dc 100644
--- a/frontend/src/components/apikeys/ProxyConfigSection.tsx
+++ b/frontend/src/components/apikeys/ProxyConfigSection.tsx
@@ -1,20 +1,49 @@
-import { useState, useEffect } from "react";
+import { useEffect, useMemo, useState } from "react";
+import {
+ AlertCircle,
+ Check,
+ CheckCircle,
+ Copy,
+ Loader2,
+ Play,
+ Server,
+ ShieldCheck,
+ Square
+} from "lucide-react";
+import { SettingsSection } from "@/components/settings/SettingsPage";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
-import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
-import { Alert, AlertDescription } from "@/components/ui/alert";
-import { Play, Square, Loader2, AlertCircle, CheckCircle, Server, Copy, Check } from "lucide-react";
-import { proxyService, ProxyConfig, ProxyStatus } from "@/services/proxyService";
+import { Switch } from "@/components/ui/switch";
+import { DEFAULT_AGENT_MODEL } from "@/services/agentModels";
+import { proxyService, type ProxyConfig, type ProxyStatus } from "@/services/proxyService";
+import { getProxyBaseUrl, isCodingAgentModel } from "@/services/proxyModels";
+import type { OpenSecretModel } from "@/state/LocalStateContextDef";
import { isTauriDesktop } from "@/utils/platform";
-import { QUICK_MODEL_ALIAS } from "@/utils/utils";
+import { ProxyClientGuides } from "./ProxyClientGuides";
+import { ProxyModelList } from "./ProxyModelList";
interface ProxyConfigSectionProps {
apiKeys: Array<{ name: string; created_at: string }>;
onRequestNewApiKey: (name: string) => Promise;
+ models: OpenSecretModel[];
+ isModelsLoading: boolean;
+ isModelsError: boolean;
+}
+
+function isLoopbackHost(host: string): boolean {
+ return host.trim() === "127.0.0.1";
}
-export function ProxyConfigSection({ apiKeys, onRequestNewApiKey }: ProxyConfigSectionProps) {
+export function ProxyConfigSection({
+ apiKeys,
+ onRequestNewApiKey,
+ models,
+ isModelsLoading,
+ isModelsError
+}: ProxyConfigSectionProps) {
const [proxyStatus, setProxyStatus] = useState(null);
const [config, setConfig] = useState({
host: "127.0.0.1",
@@ -27,14 +56,22 @@ export function ProxyConfigSection({ apiKeys, onRequestNewApiKey }: ProxyConfigS
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
const [copied, setCopied] = useState(false);
+ const [selectedModelId, setSelectedModelId] = useState("");
const isTauriDesktopPlatform = isTauriDesktop();
+ const guideModels = useMemo(() => models.filter(isCodingAgentModel), [models]);
+
useEffect(() => {
- if (!isTauriDesktopPlatform) return;
+ if (guideModels.length === 0) {
+ setSelectedModelId("");
+ return;
+ }
- // Load saved config and status on mount
- loadProxyState();
- }, [isTauriDesktopPlatform]);
+ setSelectedModelId((current) => {
+ if (guideModels.some((model) => model.id === current)) return current;
+ return guideModels.find((model) => model.id === DEFAULT_AGENT_MODEL)?.id ?? guideModels[0].id;
+ });
+ }, [guideModels]);
const loadProxyState = async () => {
try {
@@ -50,70 +87,62 @@ export function ProxyConfigSection({ apiKeys, onRequestNewApiKey }: ProxyConfigS
}
};
+ useEffect(() => {
+ if (!isTauriDesktopPlatform) return;
+ void loadProxyState();
+ }, [isTauriDesktopPlatform]);
+
const handleStartProxy = async () => {
setIsLoading(true);
+ setMessage(null);
try {
- // If no API key is set, auto-generate one
let apiKey = config.api_key;
if (!apiKey) {
const date = new Date().toISOString().split("T")[0].replace(/-/g, "");
const keyName = `maple-desktop-${date}`;
-
- // Check if a key with this name already exists
- const existingKey = apiKeys.find((k) => k.name === keyName);
+ const existingKey = apiKeys.find((key) => key.name === keyName);
if (existingKey) {
- // Use the existing key (we don't have access to the actual key value)
setMessage({
type: "error",
text: "Please select an existing API key or create a new one"
});
- setIsLoading(false);
return;
}
- // Request a new API key
try {
apiKey = await onRequestNewApiKey(keyName);
- setConfig((prev) => ({ ...prev, api_key: apiKey }));
+ setConfig((previous) => ({ ...previous, api_key: apiKey }));
} catch {
setMessage({
type: "error",
text: "Failed to create API key. Please create an API key manually first"
});
- setIsLoading(false);
return;
}
}
- // Validate port range (1-65535)
const port = Number(config.port);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
setMessage({
type: "error",
text: `Invalid port: ${config.port}. Port must be between 1 and 65535.`
});
- setIsLoading(false);
return;
}
- // Get the backend URL from environment
const backendUrl = import.meta.env.VITE_OPEN_SECRET_API_URL || "https://enclave.trymaple.ai";
-
const updatedConfig = {
...config,
api_key: apiKey,
enabled: true,
backend_url: backendUrl,
- auto_start: config.auto_start // Preserve auto_start setting
+ auto_start: config.auto_start
};
- // The user is explicitly taking control of the shared proxy. The service
- // detaches the active Agent association only after the native start
- // succeeds, while retaining its exact key name for later revocation.
+
const status = await proxyService.startManualProxy(updatedConfig);
setProxyStatus(status);
setConfig(updatedConfig);
-
setMessage({
type: "success",
text: `Proxy is now running on ${config.host}:${config.port}`
@@ -127,13 +156,13 @@ export function ProxyConfigSection({ apiKeys, onRequestNewApiKey }: ProxyConfigS
const handleStopProxy = async () => {
setIsLoading(true);
+ setMessage(null);
try {
const status = await proxyService.stopManualProxy();
setProxyStatus(status);
- setConfig((prev) => ({ ...prev, enabled: false }));
-
- setMessage({ type: "success", text: "The proxy server has been stopped" });
+ setConfig((previous) => ({ ...previous, enabled: false }));
+ setMessage({ type: "success", text: "The proxy has stopped" });
} catch (error) {
setMessage({ type: "error", text: `Failed to stop proxy: ${error}` });
} finally {
@@ -142,236 +171,278 @@ export function ProxyConfigSection({ apiKeys, onRequestNewApiKey }: ProxyConfigS
};
const handleConfigChange = (field: keyof ProxyConfig, value: string | number | boolean) => {
- setConfig((prev) => ({ ...prev, [field]: value }));
+ setConfig((previous) => ({ ...previous, [field]: value }));
};
- const copyProxyUrl = () => {
- const url = `http://${config.host}:${config.port}/v1`;
- navigator.clipboard.writeText(url);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
+ const handleAutoStartChange = async (checked: boolean) => {
+ const updatedConfig = { ...config, auto_start: checked };
+ setConfig(updatedConfig);
+ try {
+ await proxyService.saveManualProxySettings(updatedConfig);
+ setMessage({
+ type: "success",
+ text: checked ? "Auto-start enabled" : "Auto-start disabled"
+ });
+ } catch {
+ setMessage({ type: "error", text: "Failed to save the auto-start setting" });
+ }
};
- if (!isTauriDesktopPlatform) {
- return null; // Don't show proxy config on non-desktop platforms (includes mobile)
- }
-
+ const proxyBaseUrl = getProxyBaseUrl(config.host, config.port);
const isRunning = proxyStatus?.running || false;
+ const copyProxyUrl = async () => {
+ try {
+ await navigator.clipboard.writeText(proxyBaseUrl);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (error) {
+ console.error("Failed to copy proxy URL:", error);
+ setMessage({ type: "error", text: "Failed to copy the proxy URL" });
+ }
+ };
+
+ if (!isTauriDesktopPlatform) return null;
+
return (
<>
- {/* Show message alerts */}
- {message && (
-
-
+ {isRunning
+ ? "Keep Maple open while connected tools are using this endpoint."
+ : "Start the proxy when you are ready to connect an app or coding agent."}
+
- {!isRunning ? (
-
- {isLoading ? (
-
- ) : (
-
- )}
- Start Proxy
-
- ) : (
-
- {isLoading ? (
-
- ) : (
-
- )}
- Stop Proxy
-
+
+
+
+
+ Local processes can use Maple's saved proxy credential without supplying their
+ own key. Keep the proxy on loopback, run only trusted clients, and remember that proxy
+ usage counts toward your Maple account.
+
+
+
+ {config.enable_cors && (
+
+
+
+ CORS is enabled, so browser pages may be able to reach this proxy while it is
+ running. Turn CORS off unless a browser client specifically requires it.
+
+
)}
-
- {/* CORS Toggle */}
-
-
- Enable CORS (for web clients)
-
- handleConfigChange("enable_cors", e.target.checked)}
- disabled={isRunning}
- className="h-4 w-4"
- />
-
+ {!isLoopbackHost(config.host) && (
+
+
+
+ This host may expose the proxy beyond your device. Use 127.0.0.1 unless you fully
+ understand the network and billing risk.
+
+
+ )}
- {/* Auto-start Toggle */}
-
-
-
- Auto-start proxy when app launches
-
-
- Proxy will start automatically on app launch if configured
-
-
- {
- const newConfig = { ...config, auto_start: e.target.checked };
- setConfig(newConfig);
- // Save immediately when toggling auto-start
- try {
- await proxyService.saveManualProxySettings(newConfig);
- setMessage({
- type: "success",
- text: e.target.checked ? "Auto-start enabled" : "Auto-start disabled"
- });
- } catch {
- setMessage({ type: "error", text: "Failed to save auto-start setting" });
- }
- }}
- disabled={!config.api_key} // Only enable if we have an API key
- className="h-4 w-4"
- />
-
+ {models.length > 0 ? `${models.length} chat model IDs` : "Chat model IDs"}
+
+
+ Fetched from Maple's current client catalog. This confirms configured support, not
+ real-time provider uptime or every API-only model.
+
+
+
+ {isLoading && (
+
+
+ Loading the current Maple model catalog...
+
+ )}
+
+ {isError && (
+
+
+
+ Maple could not load the current model catalog. Restart Maple once you are back online.
+
+
+ )}
+
+ {!isLoading && !isError && models.length === 0 && (
+
+ No chat models are currently listed. Restart Maple if this persists.
+