feat: move background processes to its own daemon

This commit is contained in:
zhom
2026-01-11 21:01:09 +04:00
parent 6756f88955
commit eeea15c65d
39 changed files with 3466 additions and 948 deletions
+14
View File
@@ -14,6 +14,7 @@ import { GroupBadges } from "@/components/group-badges";
import { GroupManagementDialog } from "@/components/group-management-dialog";
import HomeHeader from "@/components/home-header";
import { ImportProfileDialog } from "@/components/import-profile-dialog";
import { IntegrationsDialog } from "@/components/integrations-dialog";
import { PermissionDialog } from "@/components/permission-dialog";
import { ProfilesDataTable } from "@/components/profile-data-table";
import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
@@ -88,6 +89,7 @@ export default function Home() {
const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false);
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
const [integrationsDialogOpen, setIntegrationsDialogOpen] = useState(false);
const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false);
const [proxyManagementDialogOpen, setProxyManagementDialogOpen] =
useState(false);
@@ -805,6 +807,7 @@ export default function Home() {
onProxyManagementDialogOpen={setProxyManagementDialogOpen}
onSettingsDialogOpen={setSettingsDialogOpen}
onSyncConfigDialogOpen={setSyncConfigDialogOpen}
onIntegrationsDialogOpen={setIntegrationsDialogOpen}
searchQuery={searchQuery}
onSearchQueryChange={setSearchQuery}
/>
@@ -855,6 +858,17 @@ export default function Home() {
onClose={() => {
setSettingsDialogOpen(false);
}}
onIntegrationsOpen={() => {
setSettingsDialogOpen(false);
setIntegrationsDialogOpen(true);
}}
/>
<IntegrationsDialog
isOpen={integrationsDialogOpen}
onClose={() => {
setIntegrationsDialogOpen(false);
}}
/>
<ImportProfileDialog
+11 -1
View File
@@ -1,7 +1,7 @@
import { FaDownload } from "react-icons/fa";
import { FiWifi } from "react-icons/fi";
import { GoGear, GoKebabHorizontal, GoPlus } from "react-icons/go";
import { LuCloud, LuSearch, LuUsers, LuX } from "react-icons/lu";
import { LuCloud, LuPlug, LuSearch, LuUsers, LuX } from "react-icons/lu";
import { Logo } from "./icons/logo";
import { Button } from "./ui/button";
import { CardTitle } from "./ui/card";
@@ -21,6 +21,7 @@ type Props = {
onImportProfileDialogOpen: (open: boolean) => void;
onCreateProfileDialogOpen: (open: boolean) => void;
onSyncConfigDialogOpen: (open: boolean) => void;
onIntegrationsDialogOpen: (open: boolean) => void;
searchQuery: string;
onSearchQueryChange: (query: string) => void;
};
@@ -32,6 +33,7 @@ const HomeHeader = ({
onImportProfileDialogOpen,
onCreateProfileDialogOpen,
onSyncConfigDialogOpen,
onIntegrationsDialogOpen,
searchQuery,
onSearchQueryChange,
}: Props) => {
@@ -128,6 +130,14 @@ const HomeHeader = ({
<LuCloud className="mr-2 w-4 h-4" />
Sync Service
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
onIntegrationsDialogOpen(true);
}}
>
<LuPlug className="mr-2 w-4 h-4" />
Integrations
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
onImportProfileDialogOpen(true);
+390
View File
@@ -0,0 +1,390 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { Eye, EyeOff } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useWayfernTerms } from "@/hooks/use-wayfern-terms";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
import { CopyToClipboard } from "./ui/copy-to-clipboard";
interface AppSettings {
api_enabled: boolean;
api_port: number;
api_token?: string;
mcp_enabled: boolean;
mcp_port?: number;
mcp_token?: string;
}
interface McpConfig {
port: number;
token: string;
config_json: string;
}
interface IntegrationsDialogProps {
isOpen: boolean;
onClose: () => void;
}
export function IntegrationsDialog({
isOpen,
onClose,
}: IntegrationsDialogProps) {
const [settings, setSettings] = useState<AppSettings>({
api_enabled: false,
api_port: 10108,
api_token: undefined,
mcp_enabled: false,
mcp_port: undefined,
mcp_token: undefined,
});
const [apiServerPort, setApiServerPort] = useState<number | null>(null);
const [mcpConfig, setMcpConfig] = useState<McpConfig | null>(null);
const [_mcpRunning, setMcpRunning] = useState(false);
const [showApiToken, setShowApiToken] = useState(false);
const [showMcpToken, setShowMcpToken] = useState(false);
const [isApiStarting, setIsApiStarting] = useState(false);
const [isMcpStarting, setIsMcpStarting] = useState(false);
const { termsAccepted } = useWayfernTerms();
const loadSettings = useCallback(async () => {
try {
const loaded = await invoke<AppSettings>("get_app_settings");
setSettings(loaded);
} catch (e) {
console.error("Failed to load settings:", e);
}
}, []);
const loadMcpConfig = useCallback(async () => {
try {
const config = await invoke<McpConfig | null>("get_mcp_config");
setMcpConfig(config);
} catch (e) {
console.error("Failed to get MCP config:", e);
}
}, []);
const loadMcpServerStatus = useCallback(async () => {
try {
const isRunning = await invoke<boolean>("get_mcp_server_status");
setMcpRunning(isRunning);
} catch (e) {
console.error("Failed to get MCP server status:", e);
}
}, []);
const loadApiServerStatus = useCallback(async () => {
try {
const port = await invoke<number | null>("get_api_server_status");
setApiServerPort(port);
} catch (e) {
console.error("Failed to get API server status:", e);
}
}, []);
useEffect(() => {
if (isOpen) {
loadSettings();
loadApiServerStatus();
loadMcpConfig();
loadMcpServerStatus();
}
}, [
isOpen,
loadSettings,
loadApiServerStatus,
loadMcpConfig,
loadMcpServerStatus,
]);
const handleApiToggle = async (enabled: boolean) => {
setIsApiStarting(true);
try {
if (enabled) {
const port = await invoke<number>("start_api_server", {
port: settings.api_port,
});
setApiServerPort(port);
const next = await invoke<AppSettings>("save_app_settings", {
settings: { ...settings, api_enabled: true },
});
setSettings(next);
showSuccessToast(`API server started on port ${port}`);
} else {
await invoke("stop_api_server");
setApiServerPort(null);
const next = await invoke<AppSettings>("save_app_settings", {
settings: { ...settings, api_enabled: false, api_token: null },
});
setSettings(next);
showSuccessToast("API server stopped");
}
} catch (e) {
console.error("Failed to toggle API:", e);
showErrorToast("Failed to toggle API server", {
description: e instanceof Error ? e.message : "Unknown error",
});
} finally {
setIsApiStarting(false);
}
};
const handleMcpToggle = async (enabled: boolean) => {
setIsMcpStarting(true);
try {
if (enabled) {
const port = await invoke<number>("start_mcp_server");
const next = await invoke<AppSettings>("save_app_settings", {
settings: { ...settings, mcp_enabled: true, mcp_port: port },
});
setSettings(next);
loadMcpConfig();
showSuccessToast(`MCP server started on port ${port}`);
} else {
await invoke("stop_mcp_server");
const next = await invoke<AppSettings>("save_app_settings", {
settings: { ...settings, mcp_enabled: false },
});
setSettings(next);
setMcpConfig(null);
showSuccessToast("MCP server stopped");
}
} catch (e) {
console.error("Failed to toggle MCP server:", e);
showErrorToast("Failed to toggle MCP server", {
description: e instanceof Error ? e.message : "Unknown error",
});
} finally {
setIsMcpStarting(false);
}
};
const obfuscateToken = (token: string) =>
"•".repeat(Math.min(token.length, 32));
const getFormattedMcpConfig = () => {
if (!mcpConfig) return "";
return JSON.stringify(
{
mcpServers: {
"donut-browser": {
url: `http://127.0.0.1:${mcpConfig.port}/mcp`,
headers: {
Authorization: `Bearer ${mcpConfig.token}`,
},
},
},
},
null,
2,
);
};
const getObfuscatedMcpConfig = () => {
if (!mcpConfig) return "";
return JSON.stringify(
{
mcpServers: {
"donut-browser": {
url: `http://127.0.0.1:${mcpConfig.port}/mcp`,
headers: {
Authorization: `Bearer ${obfuscateToken(mcpConfig.token)}`,
},
},
},
},
null,
2,
);
};
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle>Integrations</DialogTitle>
</DialogHeader>
<Tabs defaultValue="api" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="api">Local API</TabsTrigger>
<TabsTrigger value="mcp">MCP (AI Assistants)</TabsTrigger>
</TabsList>
<TabsContent value="api" className="space-y-4 mt-4">
<div className="flex items-center space-x-2">
<Checkbox
id="api-enabled"
checked={apiServerPort !== null}
disabled={isApiStarting}
onCheckedChange={handleApiToggle}
/>
<div className="grid gap-1.5 leading-none">
<Label
htmlFor="api-enabled"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Enable Local API Server
</Label>
<p className="text-xs text-muted-foreground">
Allow managing profiles, groups, and proxies via REST API.
</p>
</div>
</div>
{settings.api_enabled && (
<div className="space-y-4 p-4 rounded-md border bg-muted/40">
<div className="space-y-2">
<Label className="text-sm font-medium">Port</Label>
<div className="flex items-center space-x-2">
<Input
value={apiServerPort ?? settings.api_port}
readOnly
className="w-24 font-mono"
/>
<span className="text-xs text-muted-foreground">
Server is running
</span>
</div>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium">
Authentication Token
</Label>
<div className="flex items-center space-x-2">
<div className="relative flex-1">
<Input
type={showApiToken ? "text" : "password"}
value={settings.api_token ?? ""}
readOnly
className="font-mono pr-10"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
onClick={() => setShowApiToken(!showApiToken)}
>
{showApiToken ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
<CopyToClipboard
text={settings.api_token ?? ""}
successMessage="Token copied"
/>
</div>
<p className="text-xs text-muted-foreground">
Include in Authorization header: Bearer {"<token>"}
</p>
</div>
</div>
)}
</TabsContent>
<TabsContent value="mcp" className="space-y-4 mt-4">
<div className="flex items-center space-x-2">
<Checkbox
id="mcp-enabled"
checked={settings.mcp_enabled && mcpConfig !== null}
disabled={!termsAccepted || isMcpStarting}
onCheckedChange={handleMcpToggle}
/>
<div className="grid gap-1.5 leading-none">
<Label
htmlFor="mcp-enabled"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Enable MCP Server (Model Context Protocol)
</Label>
<p className="text-xs text-muted-foreground">
Allow AI assistants like Claude Desktop to control browsers.
{!termsAccepted && (
<span className="ml-1 text-orange-600">
(Accept Wayfern terms in Settings first)
</span>
)}
</p>
</div>
</div>
{mcpConfig && (
<div className="space-y-4 p-4 rounded-md border bg-muted/40">
<div className="space-y-2">
<Label className="text-sm font-medium">
Claude Desktop Configuration
</Label>
<p className="text-xs text-muted-foreground">
Copy this configuration to your Claude Desktop config file
at{" "}
<code className="bg-muted px-1 rounded">
~/.config/claude/claude_desktop_config.json
</code>
</p>
</div>
<div className="relative">
<pre className="p-3 text-xs font-mono rounded-md bg-background border overflow-x-auto whitespace-pre">
{showMcpToken
? getFormattedMcpConfig()
: getObfuscatedMcpConfig()}
</pre>
<div className="absolute top-2 right-2 flex items-center space-x-1">
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
onClick={() => setShowMcpToken(!showMcpToken)}
>
{showMcpToken ? (
<EyeOff className="h-3.5 w-3.5" />
) : (
<Eye className="h-3.5 w-3.5" />
)}
</Button>
<CopyToClipboard
text={getFormattedMcpConfig()}
successMessage="Configuration copied"
/>
</div>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium">Available Tools</Label>
<ul className="list-disc ml-5 space-y-0.5 text-xs text-muted-foreground">
<li>list_profiles - List browser profiles</li>
<li>run_profile - Launch a browser</li>
<li>kill_profile - Stop a running browser</li>
<li>get_profile_status - Check if browser is running</li>
<li>list_groups, create_group, etc. - Manage groups</li>
<li>list_proxies, create_proxy, etc. - Manage proxies</li>
</ul>
</div>
</div>
)}
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
);
}
+9 -60
View File
@@ -68,6 +68,7 @@ import {
getBrowserIcon,
getCurrentOS,
} from "@/lib/browser-utils";
import { formatRelativeTime } from "@/lib/flag-utils";
import { trimName } from "@/lib/name-utils";
import { cn } from "@/lib/utils";
import type {
@@ -1911,59 +1912,26 @@ export function ProfilesDataTable({
id: "sync",
header: "",
size: 24,
cell: ({ row, table }) => {
const meta = table.options.meta as TableMeta;
cell: ({ row }) => {
const profile = row.original;
if (!profile.sync_enabled) {
if (!profile.sync_enabled && profile.last_sync) {
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex justify-center items-center w-3 h-3">
<span className="w-2 h-2 rounded-full bg-muted-foreground/30" />
<span className="w-2 h-2 rounded-full bg-orange-500" />
</span>
</TooltipTrigger>
<TooltipContent>Sync disabled</TooltipContent>
<TooltipContent>
Sync is disabled, last sync{" "}
{formatRelativeTime(profile.last_sync)}
</TooltipContent>
</Tooltip>
);
}
const syncStatus = meta.syncStatuses[profile.id];
const isSyncing = syncStatus === "syncing";
const isWaiting = syncStatus === "waiting";
const isSynced =
syncStatus === "synced" || (!syncStatus && profile.last_sync);
const isError = syncStatus === "error";
let dotClass = "bg-yellow-500";
let tooltipText = "Sync pending";
if (isSyncing) {
dotClass = "bg-yellow-500 animate-pulse";
tooltipText = "Syncing...";
} else if (isWaiting) {
dotClass = "bg-yellow-500";
tooltipText = "Waiting for profile to stop";
} else if (isError) {
dotClass = "bg-red-500";
tooltipText = "Sync error";
} else if (isSynced) {
dotClass = "bg-green-500";
tooltipText = profile.last_sync
? `Last synced: ${new Date(profile.last_sync * 1000).toLocaleString()}`
: "Synced";
}
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex justify-center items-center w-3 h-3">
<span className={`w-2 h-2 rounded-full ${dotClass}`} />
</span>
</TooltipTrigger>
<TooltipContent>{tooltipText}</TooltipContent>
</Tooltip>
);
return null;
},
},
{
@@ -2031,25 +1999,6 @@ export function ProfilesDataTable({
Copy Cookies to Profile
</DropdownMenuItem>
)}
{meta.onOpenProfileSyncDialog && (
<DropdownMenuItem
onClick={() => {
meta.onOpenProfileSyncDialog?.(profile);
}}
>
Sync Settings
</DropdownMenuItem>
)}
{meta.onToggleProfileSync && (
<DropdownMenuItem
onClick={() => {
meta.onToggleProfileSync?.(profile);
}}
disabled={isDisabled}
>
{profile.sync_enabled ? "Disable Sync" : "Enable Sync"}
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => {
setProfileToDelete(profile);
+21 -418
View File
@@ -7,7 +7,6 @@ import { useCallback, useEffect, useState } from "react";
import { BsCamera, BsMic } from "react-icons/bs";
import { LoadingButton } from "@/components/loading-button";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import {
ColorPicker,
ColorPickerAlpha,
@@ -40,7 +39,6 @@ import {
import { useCommercialTrial } from "@/hooks/use-commercial-trial";
import type { PermissionType } from "@/hooks/use-permissions";
import { usePermissions } from "@/hooks/use-permissions";
import { useWayfernTerms } from "@/hooks/use-wayfern-terms";
import {
getThemeByColors,
getThemeById,
@@ -48,7 +46,6 @@ import {
THEMES,
} from "@/lib/themes";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
import { CopyToClipboard } from "./ui/copy-to-clipboard";
import { RippleButton } from "./ui/ripple";
interface AppSettings {
@@ -76,9 +73,14 @@ interface PermissionInfo {
interface SettingsDialogProps {
isOpen: boolean;
onClose: () => void;
onIntegrationsOpen?: () => void;
}
export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
export function SettingsDialog({
isOpen,
onClose,
onIntegrationsOpen,
}: SettingsDialogProps) {
const [settings, setSettings] = useState<AppSettings>({
set_as_default_browser: false,
theme: "system",
@@ -109,7 +111,6 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
const [requestingPermission, setRequestingPermission] =
useState<PermissionType | null>(null);
const [isMacOS, setIsMacOS] = useState(false);
const [apiServerPort, setApiServerPort] = useState<number | null>(null);
const { setTheme } = useTheme();
const {
@@ -117,10 +118,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
isMicrophoneAccessGranted,
isCameraAccessGranted,
} = usePermissions();
const { termsAccepted } = useWayfernTerms();
const { trialStatus } = useCommercialTrial();
const [mcpEnabled, setMcpEnabled] = useState(false);
const [isMcpStarting, setIsMcpStarting] = useState(false);
const getPermissionIcon = useCallback((type: PermissionType) => {
switch (type) {
@@ -352,48 +350,6 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
} catch {}
}
// Handle API server start/stop based on settings
const wasApiEnabled = originalSettings.api_enabled;
const isApiEnabled = settingsToSave.api_enabled;
if (isApiEnabled && !wasApiEnabled) {
// Start API server
try {
const port = await invoke<number>("start_api_server", {
port: settingsToSave.api_port,
});
setApiServerPort(port);
showSuccessToast(`Local API started on port ${port}`);
} catch (error) {
console.error("Failed to start API server:", error);
showErrorToast("Failed to start API server", {
description:
error instanceof Error ? error.message : "Unknown error occurred",
});
// Revert the API enabled setting if start failed
settingsToSave.api_enabled = false;
const revertedSettings = await invoke<AppSettings>(
"save_app_settings",
{ settings: settingsToSave },
);
setSettings(revertedSettings);
settingsToSave = revertedSettings;
}
} else if (!isApiEnabled && wasApiEnabled) {
// Stop API server
try {
await invoke("stop_api_server");
setApiServerPort(null);
showSuccessToast("Local API stopped");
} catch (error) {
console.error("Failed to stop API server:", error);
showErrorToast("Failed to stop API server", {
description:
error instanceof Error ? error.message : "Unknown error occurred",
});
}
}
setOriginalSettings(settingsToSave);
onClose();
} catch (error) {
@@ -401,7 +357,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
} finally {
setIsSaving(false);
}
}, [onClose, setTheme, settings, customThemeState, originalSettings]);
}, [onClose, setTheme, settings, customThemeState]);
const updateSetting = useCallback(
(
@@ -413,26 +369,6 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
[],
);
const loadApiServerStatus = useCallback(async () => {
try {
const port = await invoke<number | null>("get_api_server_status");
setApiServerPort(port);
} catch (error) {
console.error("Failed to load API server status:", error);
setApiServerPort(null);
}
}, []);
const loadMcpServerStatus = useCallback(async () => {
try {
const isRunning = await invoke<boolean>("get_mcp_server_status");
setMcpEnabled(isRunning);
} catch (error) {
console.error("Failed to load MCP server status:", error);
setMcpEnabled(false);
}
}, []);
const handleClose = useCallback(() => {
// Restore original theme when closing without saving
if (originalSettings.theme === "custom" && originalSettings.custom_theme) {
@@ -470,8 +406,6 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
if (isOpen) {
loadSettings().catch(console.error);
checkDefaultBrowserStatus().catch(console.error);
loadApiServerStatus().catch(console.error);
loadMcpServerStatus().catch(console.error);
// Check if we're on macOS
const userAgent = navigator.userAgent;
@@ -492,14 +426,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
clearInterval(intervalId);
};
}
}, [
isOpen,
loadPermissions,
checkDefaultBrowserStatus,
loadSettings,
loadApiServerStatus,
loadMcpServerStatus,
]);
}, [isOpen, loadPermissions, checkDefaultBrowserStatus, loadSettings]);
// Update permissions when the permission states change
useEffect(() => {
@@ -790,279 +717,20 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
</div>
)}
{/* Local API Section */}
{/* Integrations Section */}
<div className="space-y-4">
<Label className="text-base font-medium">Local API</Label>
<div className="flex items-center space-x-2">
<Checkbox
id="api-enabled"
checked={settings.api_enabled}
onCheckedChange={async (checked: boolean) => {
updateSetting("api_enabled", checked);
try {
if (checked) {
// Ask backend to enable API and return settings with token
const next = await invoke<AppSettings>(
"save_app_settings",
{
settings: { ...settings, api_enabled: true },
},
);
setSettings(next);
} else {
const next = await invoke<AppSettings>(
"save_app_settings",
{
settings: {
...settings,
api_enabled: false,
api_token: null,
},
},
);
setSettings(next);
}
} catch (e) {
console.error("Failed to toggle API:", e);
}
}}
/>
<div className="grid gap-1.5 leading-none">
<Label
htmlFor="api-enabled"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
(ALPHA) Enable Local API Server
</Label>
<p className="text-xs text-muted-foreground">
Allow managing the application data externally via REST API.
Server will start on port 10108 or a random port if
unavailable.
{apiServerPort && (
<span className="ml-1 font-medium text-green-600">
(Currently running on port {apiServerPort})
</span>
)}
</p>
</div>
</div>
{settings.api_enabled && settings.api_token && (
<div className="space-y-2">
<Label className="text-sm font-medium">
API Authentication Token
</Label>
<div className="flex items-center space-x-2">
<input
type="text"
value={settings.api_token}
readOnly
className="flex-1 px-3 py-2 font-mono text-sm rounded-md border bg-muted"
/>
<CopyToClipboard
text={settings.api_token || ""}
successMessage="API token copied to clipboard"
/>
</div>
<p className="text-xs text-muted-foreground">
Include this token in the Authorization header as "Bearer{" "}
{settings.api_token}" for all API requests.
</p>
{/* Temporary in-app API docs */}
<div className="p-3 mt-3 space-y-2 text-xs leading-relaxed rounded-md border bg-muted/40">
<div className="font-medium">
Temporary in-app API docs (alpha)
</div>
<div>
<div>
Base URL:{" "}
<code className="font-mono">{`http://127.0.0.1:${apiServerPort ?? settings.api_port ?? 10108}/v1`}</code>
</div>
<div>
Auth:{" "}
<code className="font-mono">
Authorization: Bearer {settings.api_token}
</code>
</div>
</div>
<div className="space-y-1">
<div className="font-medium">Profiles</div>
<ul className="list-disc ml-5 space-y-0.5">
<li>
<code className="font-mono">GET /profiles</code> list
profiles
</li>
<li>
<code className="font-mono">
GET /profiles/{"{"}id{"}"}
</code>{" "}
get one
</li>
<li>
<code className="font-mono">POST /profiles</code>
create
<span className="ml-1 text-muted-foreground">
(required: name, browser, version; optional:
release_type, proxy_id, camoufox_config, group_id,
tags)
</span>
</li>
<li>
<code className="font-mono">
PUT /profiles/{"{"}id{"}"}
</code>{" "}
update
<span className="ml-1 text-muted-foreground">
(any of: name, version, proxy_id, camoufox_config,
group_id, tags)
</span>
</li>
<li>
<code className="font-mono">
DELETE /profiles/{"{"}id{"}"}
</code>{" "}
delete
</li>
<li>
<code className="font-mono">
POST /profiles/{"{"}id{"}"}/run
</code>{" "}
launch with remote debugging
<span className="ml-1 text-muted-foreground">
(body: {"{"}url?, headless?{"}"})
</span>
</li>
<li>
<code className="font-mono">
POST /profiles/{"{"}id{"}"}/open-url
</code>{" "}
open URL in running profile
<span className="ml-1 text-muted-foreground">
(body: {"{"}url{"}"})
</span>
</li>
<li>
<code className="font-mono">
POST /profiles/{"{"}id{"}"}/kill
</code>{" "}
stop browser process
</li>
</ul>
</div>
<div className="space-y-1">
<div className="font-medium">Groups</div>
<ul className="list-disc ml-5 space-y-0.5">
<li>
<code className="font-mono">GET /groups</code> list
</li>
<li>
<code className="font-mono">
GET /groups/{"{"}id{"}"}
</code>{" "}
get one
</li>
<li>
<code className="font-mono">POST /groups</code> create
<span className="ml-1 text-muted-foreground">
(required: name)
</span>
</li>
<li>
<code className="font-mono">
PUT /groups/{"{"}id{"}"}
</code>{" "}
rename
<span className="ml-1 text-muted-foreground">
(required: name)
</span>
</li>
<li>
<code className="font-mono">
DELETE /groups/{"{"}id{"}"}
</code>{" "}
delete
</li>
</ul>
</div>
<div className="space-y-1">
<div className="font-medium">Tags</div>
<ul className="list-disc ml-5 space-y-0.5">
<li>
<code className="font-mono">GET /tags</code> list
</li>
</ul>
</div>
<div className="space-y-1">
<div className="font-medium">Proxies</div>
<ul className="list-disc ml-5 space-y-0.5">
<li>
<code className="font-mono">GET /proxies</code> list
</li>
<li>
<code className="font-mono">
GET /proxies/{"{"}id{"}"}
</code>{" "}
get one
</li>
<li>
<code className="font-mono">POST /proxies</code>
create
<span className="ml-1 text-muted-foreground">
(required: name, proxy_settings object)
</span>
</li>
<li>
<code className="font-mono">
PUT /proxies/{"{"}id{"}"}
</code>{" "}
update
<span className="ml-1 text-muted-foreground">
(optional: name, proxy_settings)
</span>
</li>
<li>
<code className="font-mono">
DELETE /proxies/{"{"}id{"}"}
</code>{" "}
delete
</li>
</ul>
</div>
<div className="space-y-1">
<div className="font-medium">Browsers</div>
<ul className="list-disc ml-5 space-y-0.5">
<li>
<code className="font-mono">
POST /browsers/download
</code>{" "}
download
<span className="ml-1 text-muted-foreground">
(required: browser, version)
</span>
</li>
<li>
<code className="font-mono">
GET /browsers/{"{"}browser{"}"}/versions
</code>{" "}
list versions
</li>
<li>
<code className="font-mono">
GET /browsers/{"{"}browser{"}"}/versions/{"{"}version
{"}"}/downloaded
</code>{" "}
is downloaded
</li>
</ul>
</div>
<div className="text-muted-foreground">
These docs are temporary and will be replaced with full
documentation later.
</div>
</div>
</div>
)}
<Label className="text-base font-medium">Integrations</Label>
<p className="text-xs text-muted-foreground">
Configure Local API and MCP (Model Context Protocol) for
integrating with external tools and AI assistants.
</p>
<RippleButton
variant="outline"
className="w-full"
onClick={onIntegrationsOpen}
>
Open Integrations Settings
</RippleButton>
</div>
{/* Commercial License Section */}
@@ -1094,71 +762,6 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
</div>
</div>
{/* MCP Server Section */}
<div className="space-y-4">
<Label className="text-base font-medium">MCP Server</Label>
<div className="flex items-center space-x-2">
<Checkbox
id="mcp-enabled"
checked={mcpEnabled}
disabled={!termsAccepted || isMcpStarting}
onCheckedChange={async (checked: boolean) => {
setIsMcpStarting(true);
try {
if (checked) {
await invoke("start_mcp_server");
setMcpEnabled(true);
showSuccessToast("MCP server started");
} else {
await invoke("stop_mcp_server");
setMcpEnabled(false);
showSuccessToast("MCP server stopped");
}
} catch (e) {
console.error("Failed to toggle MCP server:", e);
showErrorToast("Failed to toggle MCP server", {
description:
e instanceof Error ? e.message : "Unknown error",
});
} finally {
setIsMcpStarting(false);
}
}}
/>
<div className="grid gap-1.5 leading-none">
<Label
htmlFor="mcp-enabled"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Enable MCP Server (Model Context Protocol)
</Label>
<p className="text-xs text-muted-foreground">
Allow AI assistants to control Wayfern and Camoufox browsers
via MCP.
{!termsAccepted && (
<span className="ml-1 text-orange-600">
(Accept terms first)
</span>
)}
</p>
</div>
</div>
{mcpEnabled && (
<div className="p-3 space-y-2 text-xs rounded-md border bg-muted/40">
<div className="font-medium">Available MCP Tools</div>
<ul className="list-disc ml-5 space-y-0.5 text-muted-foreground">
<li>list_profiles - List Wayfern/Camoufox profiles</li>
<li>run_profile - Launch a browser profile</li>
<li>kill_profile - Stop a running browser</li>
<li>get_profile - Get profile details</li>
<li>list_proxies - List configured proxies</li>
</ul>
</div>
)}
</div>
{/* Advanced Section */}
<div className="space-y-4">
<Label className="text-base font-medium">Advanced</Label>