/* oxlint-disable react/forbid-elements -- plugin component uses raw HTML controls per SDK convention; see @executor-js/plugin-example for the same pattern */ /** * @executor-js/plugin-desktop-settings/client * * A single page mounted at `/plugins/desktop-settings/` that lets the user * inspect and configure the Electron sidecar's server connection. Talks to * the main process via `window.executor.*` (exposed by * `apps/desktop/src/preload`). * * The plugin is bundled into apps/local's renderer too (because executor * web + desktop share the same client bundle pipeline), but the page * only registers a nav entry when `window.executor` is present at * module-init time — so the web UI doesn't show a non-functional link. */ import { useCallback, useEffect, useState } from "react"; import { defineClientPlugin } from "@executor-js/sdk/client"; // --------------------------------------------------------------------------- // Shape of the values the desktop preload exposes. Kept inline rather than // imported from @executor-js/plugin-desktop-settings (or a shared package) // so this client bundle has no runtime dependency on the Electron // surface — when `window.executor` is undefined (web), the page silently // no-ops instead of crashing. // --------------------------------------------------------------------------- interface DesktopServerSettings { readonly port: number; readonly requireAuth: boolean; readonly password: string; } interface DesktopServerConnection { readonly kind: "desktop-sidecar"; readonly key: "desktop-sidecar"; readonly origin: string; readonly apiBaseUrl: string; readonly displayName: string; readonly auth?: { readonly kind: "basic"; readonly username: string; readonly password: string; }; } interface ExecutorBridge { readonly getServerConnection: () => Promise; readonly getSettings: () => Promise; readonly updateSettings: ( patch: Partial, ) => Promise; readonly regeneratePassword: () => Promise; readonly restartServer: () => Promise; } const readBridge = (): ExecutorBridge | null => { if (typeof window === "undefined") return null; const candidate = (window as Window & { readonly executor?: ExecutorBridge }).executor; if ( !candidate || typeof candidate.getSettings !== "function" || typeof candidate.getServerConnection !== "function" ) { return null; } return candidate; }; const inDesktop = readBridge() !== null; // Normalize an IPC rejection into a user-facing string at this UI boundary. // The renderer doesn't get typed errors back from Electron's invoke channel. // We don't pull `err.message` out — the structured error doesn't help the // user, only "save failed" matters. The main process logs the full error. const describeIpcError = (_err: unknown): string => "Save failed — check the desktop console for details."; // --------------------------------------------------------------------------- // SettingsPage // --------------------------------------------------------------------------- function SettingsPage() { const bridge = readBridge(); const [connection, setConnection] = useState(null); const [settings, setSettings] = useState(null); const [draft, setDraft] = useState(null); const [status, setStatus] = useState<"idle" | "saving" | "restarting" | "error">("idle"); const [error, setError] = useState(null); useEffect(() => { if (!bridge) return; void Promise.all([bridge.getSettings(), bridge.getServerConnection()]).then( ([nextSettings, nextConnection]) => { setSettings(nextSettings); setDraft(nextSettings); setConnection(nextConnection); }, ); }, [bridge]); const restartAndRefreshConnection = useCallback(async () => { if (!bridge) return; setStatus("restarting"); // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: renderer ↔ Electron IPC, errors surface in the form try { setConnection(await bridge.restartServer()); } catch (err) { setError(describeIpcError(err)); setStatus("error"); return; } setStatus("idle"); }, [bridge]); const apply = useCallback( async (patch: Partial) => { if (!bridge) return; setStatus("saving"); setError(null); let next: DesktopServerSettings; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: renderer ↔ Electron IPC, errors surface in the form try { next = await bridge.updateSettings(patch); } catch (err) { setError(describeIpcError(err)); setStatus("error"); return; } setSettings(next); setDraft(next); await restartAndRefreshConnection(); }, [bridge, restartAndRefreshConnection], ); const regenerate = useCallback(async () => { if (!bridge) return; setStatus("saving"); let next: DesktopServerSettings; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: renderer ↔ Electron IPC try { next = await bridge.regeneratePassword(); } catch (err) { setError(describeIpcError(err)); setStatus("error"); return; } setSettings(next); setDraft(next); await restartAndRefreshConnection(); }, [bridge, restartAndRefreshConnection]); if (!bridge) { return (

Desktop server settings

Open this page from Executor Desktop to inspect and change the active server connection.

); } if (!settings || !draft || !connection) { return
Loading…
; } const dirty = draft.port !== settings.port || draft.requireAuth !== settings.requireAuth || draft.password !== settings.password; const authLabel = connection.auth?.kind === "basic" ? `Basic auth as ${connection.auth.username}` : "No HTTP auth"; const cliProfileCommand = `executor server add desktop ${connection.origin} --default`; const cliUseCommand = connection.auth?.kind === "basic" ? `EXECUTOR_AUTH_PASSWORD=${connection.auth.password} executor tools sources --server desktop` : "executor tools sources --server desktop"; return (

Desktop server connection

{connection.displayName}

CLI profile
{cliProfileCommand} {cliUseCommand}
{draft.requireAuth && (
Password
{settings.password} {/* oxlint-disable-next-line react/forbid-elements -- plugin component uses raw HTML controls per SDK convention */}
Regenerating this changes the active server connection and invalidates existing HTTP MCP client configs.
)}
{/* oxlint-disable-next-line react/forbid-elements -- plugin component uses raw HTML controls per SDK convention */} {error && ( {error} )}
); } function ConnectionField(props: { readonly label: string; readonly value: string }) { return (
{props.label} {props.value}
); } // --------------------------------------------------------------------------- // Plugin spec // --------------------------------------------------------------------------- export default defineClientPlugin({ id: "desktop-settings", pages: [ { path: "/", component: SettingsPage, // Only contribute a nav entry when running inside the desktop app. // Web users see an empty Sources nav without a non-functional // "Settings" link. ...(inDesktop ? { nav: { label: "Settings" } } : {}), }, ], });