From 4c6f8402bae02572fe7cb393f8210d78684c9fb8 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Thu, 28 May 2026 14:48:15 -0700
Subject: [PATCH 1/2] Add app server connection selector
---
packages/app/package.json | 1 +
packages/app/src/routes/__root.tsx | 47 +-
.../app/src/web/server-connection-menu.tsx | 469 ++++++++++++++++++
packages/app/src/web/shell.tsx | 8 +-
4 files changed, 522 insertions(+), 3 deletions(-)
create mode 100644 packages/app/src/web/server-connection-menu.tsx
diff --git a/packages/app/package.json b/packages/app/package.json
index 876c1d26e..e14e9e288 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -24,6 +24,7 @@
"@executor-js/vite-plugin": "workspace:*",
"@tanstack/react-router": "catalog:",
"effect": "catalog:",
+ "lucide-react": "^1.7.0",
"react": "catalog:",
"react-dom": "catalog:"
},
diff --git a/packages/app/src/routes/__root.tsx b/packages/app/src/routes/__root.tsx
index abcef11d7..5d6fd4eba 100644
--- a/packages/app/src/routes/__root.tsx
+++ b/packages/app/src/routes/__root.tsx
@@ -1,9 +1,12 @@
import React from "react";
import { createRootRoute } from "@tanstack/react-router";
import { ExecutorProvider } from "@executor-js/react/api/provider";
+import { useExecutorServerConnection } from "@executor-js/react/api/server-connection";
import { ExecutorPluginsProvider } from "@executor-js/sdk/client";
+import { Button } from "@executor-js/react/components/button";
import { Toaster } from "@executor-js/react/components/sonner";
import { plugins as clientPlugins } from "virtual:executor/plugins-client";
+import { ServerConnectionMenu } from "../web/server-connection-menu";
import { Shell } from "../web/shell";
export const Route = createRootRoute({
@@ -12,7 +15,7 @@ export const Route = createRootRoute({
function RootComponent() {
return (
-
+ }>
@@ -20,3 +23,45 @@ function RootComponent() {
);
}
+
+function ShellConnectionError() {
+ const connection = useExecutorServerConnection();
+ return (
+
+
+
+
+
+
+
+
+
+ Server unavailable
+
+
+ Could not connect to Executor
+
+
+ The selected server did not answer the initial scope request. Switch servers or retry
+ this connection.
+
+
+ {connection.origin}
+
+
+
+
+
+ );
+}
diff --git a/packages/app/src/web/server-connection-menu.tsx b/packages/app/src/web/server-connection-menu.tsx
new file mode 100644
index 000000000..6f0bfd614
--- /dev/null
+++ b/packages/app/src/web/server-connection-menu.tsx
@@ -0,0 +1,469 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { ServerIcon } from "lucide-react";
+import {
+ getExecutorServerAuthorizationHeader,
+ useExecutorServerConnection,
+ useSetExecutorServerConnection,
+ type ExecutorServerAuth,
+ type ExecutorServerConnection,
+ type ExecutorServerConnectionInput,
+} from "@executor-js/react/api/server-connection";
+import {
+ EXECUTOR_SERVER_PROFILES_STORAGE_KEY,
+ getActiveExecutorServerProfile,
+ normalizeExecutorServerProfilesSnapshot,
+ parseExecutorServerProfilesSnapshot,
+ readExecutorServerProfiles,
+ removeExecutorServerProfile,
+ selectExecutorServerProfile,
+ serializeExecutorServerProfilesSnapshot,
+ upsertExecutorServerProfile,
+ writeExecutorServerProfiles,
+ type ExecutorServerProfilesSnapshot,
+} from "@executor-js/react/api/server-profiles";
+import { Button } from "@executor-js/react/components/button";
+import { Input } from "@executor-js/react/components/input";
+import { Label } from "@executor-js/react/components/label";
+import { NativeSelect, NativeSelectOption } from "@executor-js/react/components/native-select";
+import {
+ Popover,
+ PopoverContent,
+ PopoverHeader,
+ PopoverTitle,
+ PopoverTrigger,
+} from "@executor-js/react/components/popover";
+import { cn } from "@executor-js/react/lib/utils";
+
+type AuthMode = "none" | "bearer" | "basic";
+
+interface DraftProfile {
+ readonly origin: string;
+ readonly displayName: string;
+ readonly authMode: AuthMode;
+ readonly username: string;
+ readonly secret: string;
+}
+
+const emptyDraft: DraftProfile = {
+ origin: "",
+ displayName: "",
+ authMode: "none",
+ username: "executor",
+ secret: "",
+};
+
+interface DesktopProfileStorageBridge {
+ readonly getServerProfiles: () => Promise;
+ readonly setServerProfiles: (value: string) => Promise;
+}
+
+const browserStorage = () => (typeof window === "undefined" ? null : window.localStorage);
+
+const desktopProfileStorageBridge = (): DesktopProfileStorageBridge | null => {
+ if (typeof window === "undefined") return null;
+ const bridge = window.executor;
+ if (
+ !bridge ||
+ typeof bridge.getServerProfiles !== "function" ||
+ typeof bridge.setServerProfiles !== "function"
+ ) {
+ return null;
+ }
+ return {
+ getServerProfiles: bridge.getServerProfiles,
+ setServerProfiles: bridge.setServerProfiles,
+ };
+};
+
+const readBrowserProfiles = (): ExecutorServerProfilesSnapshot =>
+ readExecutorServerProfiles(browserStorage());
+
+const clearBrowserProfiles = (): void => {
+ if (typeof window === "undefined") return;
+ window.localStorage.removeItem(EXECUTOR_SERVER_PROFILES_STORAGE_KEY);
+};
+
+const readStoredProfiles = (): Promise => {
+ const bridge = desktopProfileStorageBridge();
+ const browserProfiles = readBrowserProfiles();
+ if (!bridge) return Promise.resolve(browserProfiles);
+
+ return bridge.getServerProfiles().then(
+ (raw) => {
+ const desktopProfiles = parseExecutorServerProfilesSnapshot(raw);
+ if (desktopProfiles.profiles.length > 0) {
+ if (browserProfiles.profiles.length > 0) {
+ const mergedProfiles = normalizeExecutorServerProfilesSnapshot({
+ activeKey: desktopProfiles.activeKey ?? browserProfiles.activeKey,
+ profiles: [...browserProfiles.profiles, ...desktopProfiles.profiles],
+ });
+ void bridge
+ .setServerProfiles(serializeExecutorServerProfilesSnapshot(mergedProfiles))
+ .then(clearBrowserProfiles, () => undefined);
+ return mergedProfiles;
+ }
+ clearBrowserProfiles();
+ return desktopProfiles;
+ }
+ if (browserProfiles.profiles.length > 0) {
+ void bridge
+ .setServerProfiles(serializeExecutorServerProfilesSnapshot(browserProfiles))
+ .then(clearBrowserProfiles, () => undefined);
+ return browserProfiles;
+ }
+ return desktopProfiles;
+ },
+ () => browserProfiles,
+ );
+};
+
+const writeStoredProfiles = (snapshot: ExecutorServerProfilesSnapshot): void => {
+ const bridge = desktopProfileStorageBridge();
+ if (!bridge) {
+ writeExecutorServerProfiles(browserStorage(), snapshot);
+ return;
+ }
+
+ void bridge
+ .setServerProfiles(serializeExecutorServerProfilesSnapshot(snapshot))
+ .then(clearBrowserProfiles, () => undefined);
+};
+
+const serverLabel = (connection: ExecutorServerConnection): string =>
+ connection.displayName || connection.origin.replace(/^https?:\/\//, "");
+
+const serverDescription = (connection: ExecutorServerConnection): string =>
+ connection.origin.replace(/^https?:\/\//, "");
+
+const serverKindLabel = (connection: ExecutorServerConnection): string => {
+ if (connection.kind === "desktop-sidecar") return "Desktop";
+ const hostname = new URL(connection.origin).hostname;
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"
+ ? "Local"
+ : "Remote";
+};
+
+const authLabel = (connection: ExecutorServerConnection): string => {
+ const authorization = getExecutorServerAuthorizationHeader(connection);
+ if (!authorization) return "No auth";
+ if (authorization.startsWith("Bearer ")) return "Bearer";
+ if (authorization.startsWith("Basic ")) return "Basic";
+ return "Auth";
+};
+
+const snapshotWithCurrent = (
+ snapshot: ExecutorServerProfilesSnapshot,
+ connection: ExecutorServerConnection,
+ makeActive: boolean,
+): ExecutorServerProfilesSnapshot =>
+ upsertExecutorServerProfile(snapshot, connection, { makeActive }) ?? snapshot;
+
+const draftAuth = (draft: DraftProfile): ExecutorServerAuth | undefined => {
+ const secret = draft.secret.trim();
+ if (draft.authMode === "bearer" && secret) {
+ return { kind: "bearer", token: secret };
+ }
+ if (draft.authMode === "basic" && secret) {
+ return {
+ kind: "basic",
+ username: draft.username.trim() || undefined,
+ password: secret,
+ };
+ }
+ return undefined;
+};
+
+interface ServerConnectionMenuProps {
+ readonly side?: "top" | "right" | "bottom" | "left";
+ readonly align?: "start" | "center" | "end";
+ readonly variant?: "default" | "header";
+}
+
+export function ServerConnectionMenu(props: ServerConnectionMenuProps = {}) {
+ const connection = useExecutorServerConnection();
+ const setServerConnection = useSetExecutorServerConnection();
+ const hydratedRef = useRef(false);
+ const [hydrated, setHydrated] = useState(false);
+ const [snapshot, setSnapshot] = useState(() => ({
+ activeKey: connection.key,
+ profiles: [connection],
+ }));
+ const [draft, setDraft] = useState(emptyDraft);
+ const [error, setError] = useState(null);
+ const [showCustomServer, setShowCustomServer] = useState(false);
+
+ const persistSnapshot = useCallback((next: ExecutorServerProfilesSnapshot) => {
+ setSnapshot(next);
+ writeStoredProfiles(next);
+ }, []);
+
+ useEffect(() => {
+ if (hydratedRef.current) return;
+ hydratedRef.current = true;
+
+ let cancelled = false;
+ void readStoredProfiles().then((stored) => {
+ if (cancelled) return;
+ const storedActive = getActiveExecutorServerProfile(stored);
+ const next = snapshotWithCurrent(stored, connection, storedActive === null);
+ persistSnapshot(next);
+ if (storedActive && storedActive.key !== connection.key) {
+ setServerConnection(storedActive);
+ }
+ setHydrated(true);
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [connection, persistSnapshot, setServerConnection]);
+
+ useEffect(() => {
+ if (!hydrated) return;
+ setSnapshot((previous) => {
+ const next = snapshotWithCurrent(previous, connection, true);
+ writeStoredProfiles(next);
+ return next;
+ });
+ }, [connection, hydrated]);
+
+ const selectProfile = (key: string): void => {
+ const next = selectExecutorServerProfile(snapshot, key);
+ const active = getActiveExecutorServerProfile(next);
+ persistSnapshot(next);
+ if (active) setServerConnection(active);
+ };
+
+ const removeProfile = (key: string): void => {
+ if (snapshot.profiles.length <= 1) return;
+ const next = removeExecutorServerProfile(snapshot, key);
+ const active = getActiveExecutorServerProfile(next);
+ persistSnapshot(next);
+ if (key === connection.key && active) setServerConnection(active);
+ };
+
+ const addProfile = (event: React.FormEvent): void => {
+ event.preventDefault();
+ const origin = draft.origin.trim();
+ if (!origin) {
+ setError("Enter a server origin.");
+ return;
+ }
+ if (draft.authMode !== "none" && !draft.secret.trim()) {
+ setError("Enter the credential value.");
+ return;
+ }
+
+ const auth = draftAuth(draft);
+ const input: ExecutorServerConnectionInput = {
+ kind: "http",
+ origin,
+ ...(draft.displayName.trim() ? { displayName: draft.displayName.trim() } : {}),
+ ...(auth ? { auth } : {}),
+ };
+ const next = upsertExecutorServerProfile(snapshot, input);
+ const active = next ? getActiveExecutorServerProfile(next) : null;
+ if (!next || !active) {
+ setError("Enter a valid http or https origin.");
+ return;
+ }
+
+ setError(null);
+ setDraft(emptyDraft);
+ setShowCustomServer(false);
+ persistSnapshot(next);
+ setServerConnection(active);
+ };
+ const trigger =
+ props.variant === "header" ? (
+
+ ) : (
+
+ );
+
+ return (
+
+ {trigger}
+
+
+ Server profiles
+
+
+
+ {snapshot.profiles.map((profile) => {
+ const active = profile.key === connection.key;
+ const profileAuthLabel = authLabel(profile);
+ return (
+
+
+
+ {serverKindLabel(profile)}
+
+ {profileAuthLabel !== "No auth" && (
+
+ {profileAuthLabel}
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+
+ {showCustomServer && (
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/app/src/web/shell.tsx b/packages/app/src/web/shell.tsx
index 204118360..8de519220 100644
--- a/packages/app/src/web/shell.tsx
+++ b/packages/app/src/web/shell.tsx
@@ -10,6 +10,7 @@ import { Button } from "@executor-js/react/components/button";
import { SourceFavicon, sourcePresetIconUrl } from "@executor-js/react/components/source-favicon";
import { CommandPalette } from "@executor-js/react/components/command-palette";
import { useClientPlugins, useSourcePlugins } from "@executor-js/sdk/client";
+import { ServerConnectionMenu } from "./server-connection-menu";
// ── Env ─────────────────────────────────────────────────────────────────
@@ -356,13 +357,16 @@ function SidebarContent(props: {
return (
<>
{props.showBrand !== false && (
-
-
+
)}
From 4d4f02eb53b58c29f25e6f9ad863a300e85aebe1 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Thu, 28 May 2026 18:30:37 -0700
Subject: [PATCH 2/2] Avoid window typeof checks in server menu
---
packages/app/src/web/server-connection-menu.tsx | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/packages/app/src/web/server-connection-menu.tsx b/packages/app/src/web/server-connection-menu.tsx
index 6f0bfd614..0b9585825 100644
--- a/packages/app/src/web/server-connection-menu.tsx
+++ b/packages/app/src/web/server-connection-menu.tsx
@@ -57,11 +57,10 @@ interface DesktopProfileStorageBridge {
readonly setServerProfiles: (value: string) => Promise
;
}
-const browserStorage = () => (typeof window === "undefined" ? null : window.localStorage);
+const browserStorage = () => globalThis.window?.localStorage ?? null;
const desktopProfileStorageBridge = (): DesktopProfileStorageBridge | null => {
- if (typeof window === "undefined") return null;
- const bridge = window.executor;
+ const bridge = globalThis.window?.executor;
if (
!bridge ||
typeof bridge.getServerProfiles !== "function" ||
@@ -79,8 +78,7 @@ const readBrowserProfiles = (): ExecutorServerProfilesSnapshot =>
readExecutorServerProfiles(browserStorage());
const clearBrowserProfiles = (): void => {
- if (typeof window === "undefined") return;
- window.localStorage.removeItem(EXECUTOR_SERVER_PROFILES_STORAGE_KEY);
+ browserStorage()?.removeItem(EXECUTOR_SERVER_PROFILES_STORAGE_KEY);
};
const readStoredProfiles = (): Promise => {