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 { integrationInferredUrl, 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 ( {/* Same inputs as the integrations list. Passing only the slug and kind meant an icon could be found ONLY when a bundled preset happened to match — anything added from the registry has a slug no preset knows, so the sidebar sat blank while the same integration showed its mark in the list. The display URL is what a favicon is derived from. */} {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/"); const isArtifacts = props.pathname === "/artifacts" || props.pathname.startsWith("/artifacts/"); return ( <> {props.showBrand !== false && (
)} {/* Footer */}
{/* oxlint-disable-next-line react/forbid-elements */} Docs Feedback / bug? Star on GitHub v{VITE_APP_VERSION}
); } // ── 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 */} {/* Mobile sidebar overlay */} {mobileSidebarOpen && (
{/* oxlint-disable-next-line react/forbid-elements */}
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). */}
); }