/* 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 { type CSSProperties, 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; } interface DesktopServerConnection { readonly kind: "desktop-sidecar"; readonly key: "desktop-sidecar"; readonly origin: string; readonly apiBaseUrl: string; readonly displayName: string; } interface ExecutorBridge { readonly getServerConnection: () => Promise; // The bearer token, fetched on demand to display the CLI/MCP connect command. readonly getServerAuthToken?: () => Promise; readonly getSettings: () => Promise; readonly updateSettings: ( patch: Partial, ) => Promise; readonly rotateToken: () => Promise; readonly restartServer: () => Promise; // Optional: present in desktop builds that ship the diagnostics export. readonly exportDiagnostics?: () => 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."; const pageFrameStyle: CSSProperties = { minHeight: 0, height: "100%", overflowY: "auto", }; // --------------------------------------------------------------------------- // SettingsPage // --------------------------------------------------------------------------- function SettingsPage() { const bridge = readBridge(); const [connection, setConnection] = useState(null); const [settings, setSettings] = useState(null); const [draft, setDraft] = useState(null); const [authToken, setAuthToken] = 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(), bridge.getServerAuthToken?.() ?? Promise.resolve(null), ]).then(([nextSettings, nextConnection, nextToken]) => { setSettings(nextSettings); setDraft(nextSettings); setConnection(nextConnection); setAuthToken(nextToken); }); }, [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 [diagnostics, setDiagnostics] = useState< { readonly state: "idle" | "exporting" } | { readonly state: "done"; readonly path: string } >({ state: "idle" }); const exportDiagnostics = useCallback(async () => { if (!bridge?.exportDiagnostics) return; setDiagnostics({ state: "exporting" }); // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: renderer ↔ Electron IPC, errors surface in the form try { const path = await bridge.exportDiagnostics(); setDiagnostics({ state: "done", path }); } catch { setError("Diagnostics export failed — check the desktop log for details."); setDiagnostics({ state: "idle" }); } }, [bridge]); const rotate = useCallback(async () => { if (!bridge) return; setStatus("restarting"); // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: renderer ↔ Electron IPC try { setConnection(await bridge.rotateToken()); setAuthToken((await bridge.getServerAuthToken?.()) ?? null); } catch (err) { setError(describeIpcError(err)); setStatus("error"); return; } setStatus("idle"); }, [bridge]); 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; const authLabel = "Bearer token"; const cliProfileCommand = `executor server add desktop ${connection.origin} --default`; const cliUseCommand = authToken ? `EXECUTOR_AUTH_TOKEN=${authToken} executor tools sources --server desktop` : "executor tools sources --server desktop"; return (

Desktop server connection

{connection.displayName}

CLI profile
{cliProfileCommand} {cliUseCommand}
Bearer token
{authToken ?? "—"} {/* oxlint-disable-next-line react/forbid-elements -- plugin component uses raw HTML controls per SDK convention */}
The sidecar enforces this token on /api and /mcp. Rotating it restarts the connection and invalidates existing MCP client configs — re-run your connect command afterwards.
{/* oxlint-disable-next-line react/forbid-elements -- plugin component uses raw HTML controls per SDK convention */} {error && ( {error} )}
{bridge.exportDiagnostics && (
Diagnostics
Packs app and server logs, crash dumps, and version info into a zip in your Downloads folder — attach it when reporting a bug. Your sources, secrets, and bearer token are not included.
{/* oxlint-disable-next-line react/forbid-elements -- plugin component uses raw HTML controls per SDK convention */} {diagnostics.state === "done" && ( {diagnostics.path} )}
)}
); } 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" } } : {}), }, ], });