import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { useEffect, useRef, useState } from "react";
import { useAtomRefresh, useAtomValue } from "@effect/atom-react";
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
import type { Integration } from "@executor-js/sdk/shared";
import {
integrationsAtom,
integrationsOptimisticAtom,
toolsAllAtom,
} from "@executor-js/react/api/atoms";
import { Button } from "@executor-js/react/components/button";
import { integrationPresetIconUrl } from "@executor-js/react/components/integration-favicon";
import { IntegrationIconWithAccount } from "@executor-js/react/components/integration-icon-with-account";
import { CommandPalette } from "@executor-js/react/components/command-palette";
import { useClientPlugins, useIntegrationPlugins } from "@executor-js/sdk/client";
import { SidebarUpdateCard } from "@executor-js/react/components/update-card";
import { Wordmark } from "@executor-js/react/components/wordmark";
import { ServerConnectionMenu } from "./server-connection-menu";
// ── Env ─────────────────────────────────────────────────────────────────
type AppMetaEnv = {
readonly VITE_APP_VERSION: string;
readonly VITE_GITHUB_URL: string;
};
const { VITE_APP_VERSION, VITE_GITHUB_URL } = (
import.meta as ImportMeta & {
readonly env: AppMetaEnv;
}
).env;
// ── NavItem ──────────────────────────────────────────────────────────────
function NavItem(props: { to: string; label: string; active: boolean; onNavigate?: () => void }) {
return (
{props.label}
);
}
// ── PluginNav ────────────────────────────────────────────────────────────
//
// Renders one nav link per plugin page that opted in via
// `pages[].nav.label`. The catch-all `/plugins/$pluginId/$` route is the
// mount point; the splat is the page's relative path with the leading
// slash stripped.
function PluginNav(props: { pathname: string; onNavigate?: () => void }) {
const plugins = useClientPlugins();
const entries = plugins.flatMap((plugin) =>
(plugin.pages ?? [])
.filter((page) => page.nav)
.map((page) => {
const splat = page.path.replace(/^\//, "");
const href = `/plugins/${plugin.id}${splat ? `/${splat}` : "/"}`;
return {
key: `${plugin.id}:${page.path}`,
pluginId: plugin.id,
splat,
href,
label: page.nav!.label,
};
}),
);
if (entries.length === 0) return null;
return (
<>
{entries.map((entry) => (
{entry.label}
))}
>
);
}
// ── IntegrationList ───────────────────────────────────────────────────────────
function IntegrationList(props: { pathname: string; onNavigate?: () => void }) {
const integrations = useAtomValue(integrationsOptimisticAtom);
const integrationPlugins = useIntegrationPlugins();
return AsyncResult.match(integrations, {
onInitial: () =>
Loading…
,
onFailure: () => (
No integrations yet
),
onSuccess: ({ value }: { readonly value: readonly Integration[] }) =>
value.length === 0 ? (
No integrations yet
) : (
{value.map((integration: Integration) => {
const slug = String(integration.slug);
const name = integration.name || slug;
const detailPath = `/integrations/${slug}`;
const active =
props.pathname === detailPath || props.pathname.startsWith(`${detailPath}/`);
return (
{name}
);
})}
),
});
}
// ── SidebarContent ───────────────────────────────────────────────────────
function SidebarContent(props: {
pathname: string;
onNavigate?: () => void;
showBrand?: boolean;
onOpenCommands: () => void;
}) {
const isHome = props.pathname === "/";
const isSecrets = props.pathname === "/secrets";
const isPolicies = props.pathname === "/policies";
const isToolkits = props.pathname === "/toolkits" || props.pathname.startsWith("/toolkits/");
return (
<>
{props.showBrand !== false && (
)}
{/* Integrations list */}
Integrations
{/* Footer */}
>
);
}
// ── Shell ─────────────────────────────────────────────────────────────────
export function Shell() {
const location = useLocation();
const pathname = location.pathname;
const refreshIntegrations = useAtomRefresh(integrationsAtom);
const refreshTools = useAtomRefresh(toolsAllAtom);
const lastPathname = useRef(pathname);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
if (lastPathname.current !== pathname) {
lastPathname.current = pathname;
if (mobileSidebarOpen) setMobileSidebarOpen(false);
}
// Lock scroll when mobile sidebar open
useEffect(() => {
if (!mobileSidebarOpen) return;
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = prev;
};
}, [mobileSidebarOpen]);
useEffect(() => {
if (!import.meta.hot) {
return;
}
const refreshBackendData = () => {
refreshIntegrations();
refreshTools();
};
import.meta.hot.on("executor:backend-updated", refreshBackendData);
return () => {
import.meta.hot?.off("executor:backend-updated", refreshBackendData);
};
}, [refreshIntegrations, refreshTools]);
return (
{/* Desktop sidebar */}
setCommandPaletteOpen(true)} />
{/* Mobile sidebar overlay */}
{mobileSidebarOpen && (
{/* oxlint-disable-next-line react/forbid-elements */}
setMobileSidebarOpen(false)}
/>
setMobileSidebarOpen(false)}
className="desktop-macos-no-drag text-sidebar-foreground hover:bg-sidebar-active hover:text-foreground"
>
setMobileSidebarOpen(false)}
showBrand={false}
onOpenCommands={() => {
setMobileSidebarOpen(false);
setCommandPaletteOpen(true);
}}
/>
)}
{/* Main content */}
{/* Desktop (macOS frameless) draggable title-bar strip. Gives the main
area the same native window drag + double-click-to-zoom as the
sidebar header; hidden everywhere else via CSS. Overlays the top of
the main area (behind page content) so page headers stay flush with
the top and their borders line up with the sidebar header. */}
{/* Mobile top bar. The desktop-macos-titlebar offset keeps the
far-left hamburger clear of the native traffic lights when the macOS
window is forced below the md breakpoint (issue #1125). */}
setMobileSidebarOpen(true)}
className="desktop-macos-no-drag bg-card hover:bg-accent/50"
>
);
}