import { Link, Outlet, useLocation } from "@tanstack/react-router";import { useCallback, useEffect, useRef, useState } from "react";import { useAtomRefresh, useAtomValue } from "@effect/atom-react";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";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 { toast } from "@executor-js/react/components/sonner";import { copyToClipboard } from "@executor-js/react/lib/clipboard";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 { 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; // ── Version helpers ───────────────────────────────────────────────────── type UpdateChannel = "latest" | "beta"; const EXECUTOR_DIST_TAGS_PATH = "/v1/app/npm/dist-tags"; type ParsedVersion = { readonly major: number; readonly minor: number; readonly patch: number; readonly prerelease: ReadonlyArray<string | number> | null;}; const semverPattern = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:-(?<prerelease>[0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/; const resolveUpdateChannel = (version: string): UpdateChannel => version.includes("-beta.") ? "beta" : "latest"; const parseVersion = (version: string): ParsedVersion | null => { const match = version.trim().match(semverPattern); if (!match?.groups) return null; return { major: Number(match.groups.major), minor: Number(match.groups.minor), patch: Number(match.groups.patch), prerelease: match.groups.prerelease ? match.groups.prerelease.split(".").map((id) => (/^\d+$/.test(id) ? Number(id) : id)) : null, };}; const comparePrereleaseIdentifiers = ( left: ReadonlyArray<string | number> | null, right: ReadonlyArray<string | number> | null,): number => { if (left === null && right === null) return 0; if (left === null) return 1; if (right === null) return -1; const max = Math.max(left.length, right.length); for (let i = 0; i < max; i++) { const l = left[i]; const r = right[i]; if (l === undefined) return -1; if (r === undefined) return 1; if (l === r) continue; if (typeof l === "number" && typeof r === "number") return l < r ? -1 : 1; if (typeof l === "number") return -1; if (typeof r === "number") return 1; return l < r ? -1 : 1; } return 0;}; const compareVersions = (left: string, right: string): number | null => { const lv = parseVersion(left); const rv = parseVersion(right); if (!lv || !rv) return null; if (lv.major !== rv.major) return lv.major < rv.major ? -1 : 1; if (lv.minor !== rv.minor) return lv.minor < rv.minor ? -1 : 1; if (lv.patch !== rv.patch) return lv.patch < rv.patch ? -1 : 1; return comparePrereleaseIdentifiers(lv.prerelease, rv.prerelease);}; // ── useLatestVersion ──────────────────────────────────────────────────── function useLatestVersion(currentVersion: string) { const channel = resolveUpdateChannel(currentVersion); const [latestVersion, setLatestVersion] = useState<string | null>(null); useEffect(() => { let cancelled = false; void Effect.runPromiseExit( Effect.tryPromise({ try: async () => { const res = await fetch(EXECUTOR_DIST_TAGS_PATH); if (!res.ok) return null; return (await res.json()) as Partial<Record<UpdateChannel, string>>; }, catch: (cause) => cause, }), ).then((exit) => { if (!cancelled && Exit.isSuccess(exit)) { setLatestVersion(exit.value?.[channel] ?? null); } }); return () => { cancelled = true; }; }, [channel]); const updateAvailable = latestVersion !== null && compareVersions(currentVersion, latestVersion) === -1; return { latestVersion, updateAvailable, channel };} // ── UpdateCard ────────────────────────────────────────────────────────── function UpdateCard(props: { latestVersion: string; channel: UpdateChannel }) { const command = `npm i -g executor@${props.channel}`; const [copied, setCopied] = useState(false); const handleCopy = useCallback(() => { void copyToClipboard(command).then((ok) => { if (!ok) { toast.error("Failed to copy to clipboard"); return; } setCopied(true); setTimeout(() => setCopied(false), 1500); }); }, [command]); return ( <div className="mx-2 mb-2 rounded-xl border border-primary/25 bg-primary/[0.06] p-3"> <div className="flex items-center gap-2"> <div className="flex size-5 shrink-0 items-center justify-center rounded-full bg-primary/15"> <svg viewBox="0 0 16 16" fill="none" className="size-3 text-primary"> <path d="M8 3v7M5 7l3 3 3-3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" /> <path d="M3 12h10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" /> </svg> </div> <div className="min-w-0"> <p className="text-xs font-semibold text-foreground">Update available</p> <p className="text-sm text-muted-foreground">v{props.latestVersion}</p> </div> </div> <Button type="button" variant="outline" onClick={handleCopy} className="mt-2.5 flex w-full items-center justify-between gap-2 rounded-lg border-border/60 bg-background/50 px-2.5 py-1.5 text-left hover:bg-background/80" > <code className="truncate font-mono text-xs text-sidebar-foreground">{command}</code> <span className="shrink-0 text-muted-foreground transition-colors group-hover:text-foreground"> {copied ? ( <svg viewBox="0 0 16 16" fill="none" className="size-3 text-primary"> <path d="M3 8.5l3.5 3.5L13 4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" /> </svg> ) : ( <svg viewBox="0 0 16 16" fill="none" className="size-3"> <rect x="5" y="5" width="8" height="8" rx="1.5" stroke="currentColor" strokeWidth="1.2" /> <path d="M3 11V3.5A.5.5 0 013.5 3H11" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" /> </svg> )} </span> </Button> </div> );} // ── NavItem ────────────────────────────────────────────────────────────── function NavItem(props: { to: string; label: string; active: boolean; onNavigate?: () => void }) { return ( <Link to={props.to} onClick={props.onNavigate} className={[ "flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors", props.active ? "bg-sidebar-active text-foreground font-medium" : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground", ].join(" ")} > {props.label} </Link> );} // ── 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) => ( <Link key={entry.key} to="/{-$orgSlug}/plugins/$pluginId/$" params={{ pluginId: entry.pluginId, _splat: entry.splat }} onClick={props.onNavigate} className={[ "flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors", props.pathname === entry.href || props.pathname.startsWith(`${entry.href}/`) ? "bg-sidebar-active text-foreground font-medium" : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground", ].join(" ")} > {entry.label} </Link> ))} </> );} // ── IntegrationList ─────────────────────────────────────────────────────────── function IntegrationList(props: { pathname: string; onNavigate?: () => void }) { const integrations = useAtomValue(integrationsOptimisticAtom); const integrationPlugins = useIntegrationPlugins(); return AsyncResult.match(integrations, { onInitial: () => <div className="px-2.5 py-2 text-xs text-muted-foreground">Loading…</div>, onFailure: () => ( <div className="px-2.5 py-2 text-xs text-muted-foreground">No integrations yet</div> ), onSuccess: ({ value }: { readonly value: readonly Integration[] }) => value.length === 0 ? ( <div className="px-2.5 py-2 text-sm leading-relaxed text-muted-foreground"> No integrations yet </div> ) : ( <div className="flex flex-col gap-px"> {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 ( <Link key={slug} to="/{-$orgSlug}/integrations/$namespace" params={{ namespace: slug }} onClick={props.onNavigate} className={[ "group flex items-center gap-2 rounded-md px-2.5 py-1.5 text-xs transition-colors", active ? "bg-sidebar-active text-foreground font-medium" : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground", ].join(" ")} > <IntegrationIconWithAccount icon={integrationPresetIconUrl( { id: slug, kind: integration.kind }, integrationPlugins, )} sourceId={slug} size="sm" /> <span className="flex-1 truncate">{name}</span> <span className="rounded bg-secondary/50 px-1 py-px text-xs font-medium text-muted-foreground"> {integration.kind} </span> </Link> ); })} </div> ), });} // ── SidebarContent ─────────────────────────────────────────────────────── function SidebarContent(props: { pathname: string; onNavigate?: () => void; showBrand?: boolean; updateAvailable: boolean; latestVersion: string | null; channel: UpdateChannel;}) { const isHome = props.pathname === "/"; const isSecrets = props.pathname === "/secrets"; const isPolicies = props.pathname === "/policies"; return ( <> {props.showBrand !== false && ( <div className="flex h-12 shrink-0 items-center gap-2 border-b border-sidebar-border px-4"> <Link to="/{-$orgSlug}" className="flex shrink-0 items-center gap-1.5"> <span className="font-display text-base tracking-tight text-foreground">executor</span> <span className="rounded bg-primary/15 px-1.5 py-px text-[10px] font-semibold uppercase tracking-wider text-primary"> Beta </span> </Link> <div className="ml-auto flex min-w-0 flex-1 justify-end"> <ServerConnectionMenu variant="header" /> </div> </div> )} <nav className="flex flex-1 flex-col overflow-y-auto p-2"> <NavItem to="/{-$orgSlug}" label="Integrations" active={isHome} onNavigate={props.onNavigate} /> <NavItem to="/{-$orgSlug}/secrets" label="Secrets" active={isSecrets} onNavigate={props.onNavigate} /> <NavItem to="/{-$orgSlug}/policies" label="Policies" active={isPolicies} onNavigate={props.onNavigate} /> <PluginNav pathname={props.pathname} onNavigate={props.onNavigate} /> {/* Sources list */} <div className="mt-5 mb-1 px-2.5 text-xs font-medium uppercase tracking-widest text-muted-foreground"> <span>Integrations</span> </div> <IntegrationList pathname={props.pathname} onNavigate={props.onNavigate} /> </nav> {props.updateAvailable && props.latestVersion && ( <UpdateCard latestVersion={props.latestVersion} channel={props.channel} /> )} {/* Footer */} <div className="shrink-0 border-t border-sidebar-border px-4 py-2.5"> <div className="flex flex-col gap-1.5 text-xs leading-none"> <a href={`${VITE_GITHUB_URL}/issues`} target="_blank" rel="noreferrer" className="text-muted-foreground transition-colors hover:text-foreground" > Feedback / bug? </a> <a href={VITE_GITHUB_URL} target="_blank" rel="noreferrer" className="text-muted-foreground transition-colors hover:text-foreground" > Star on GitHub </a> <span className="mt-0.5 text-xs text-muted-foreground tabular-nums"> v{VITE_APP_VERSION} </span> </div> </div> </> );} // ── Shell ───────────────────────────────────────────────────────────────── export function Shell() { const location = useLocation(); const pathname = location.pathname; const refreshSources = useAtomRefresh(integrationsAtom); const refreshTools = useAtomRefresh(toolsAllAtom); const { latestVersion, updateAvailable, channel } = useLatestVersion(VITE_APP_VERSION); const lastPathname = useRef(pathname); const [mobileSidebarOpen, setMobileSidebarOpen] = 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 = () => { refreshSources(); refreshTools(); }; import.meta.hot.on("executor:backend-updated", refreshBackendData); return () => { import.meta.hot?.off("executor:backend-updated", refreshBackendData); }; }, [refreshSources, refreshTools]); return ( <div className="flex h-screen overflow-hidden"> <CommandPalette /> {/* Desktop sidebar */} <aside className="hidden w-52 shrink-0 border-r border-sidebar-border bg-sidebar md:flex md:flex-col lg:w-56"> <SidebarContent pathname={pathname} updateAvailable={updateAvailable} latestVersion={latestVersion} channel={channel} /> </aside> {/* Mobile sidebar overlay */} {mobileSidebarOpen && ( <div className="fixed inset-0 z-50 flex md:hidden"> {/* oxlint-disable-next-line react/forbid-elements */} <button type="button" aria-label="Close navigation" className="absolute inset-0 bg-black/45 backdrop-blur-[1px]" onClick={() => setMobileSidebarOpen(false)} /> <div className="relative flex h-full w-[84vw] max-w-xs flex-col border-r border-sidebar-border bg-sidebar shadow-2xl"> <div className="flex h-12 shrink-0 items-center justify-between border-b border-sidebar-border px-4"> <Link to="/{-$orgSlug}" className="flex items-center gap-1.5"> <span className="font-display text-base tracking-tight text-foreground"> executor </span> <span className="rounded bg-primary/15 px-1.5 py-px text-[10px] font-semibold uppercase tracking-wider text-primary"> Beta </span>Lines 1–500 of 561. Load more
import { Link, Outlet, useLocation } from "@tanstack/react-router"; import { useCallback, useEffect, useRef, useState } from "react"; import { useAtomRefresh, useAtomValue } from "@effect/atom-react"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; 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 { toast } from "@executor-js/react/components/sonner"; import { copyToClipboard } from "@executor-js/react/lib/clipboard"; 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 { 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; // ── Version helpers ───────────────────────────────────────────────────── type UpdateChannel = "latest" | "beta"; const EXECUTOR_DIST_TAGS_PATH = "/v1/app/npm/dist-tags"; type ParsedVersion = { readonly major: number; readonly minor: number; readonly patch: number; readonly prerelease: ReadonlyArray<string | number> | null; }; const semverPattern = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:-(?<prerelease>[0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/; const resolveUpdateChannel = (version: string): UpdateChannel => version.includes("-beta.") ? "beta" : "latest"; const parseVersion = (version: string): ParsedVersion | null => { const match = version.trim().match(semverPattern); if (!match?.groups) return null; return { major: Number(match.groups.major), minor: Number(match.groups.minor), patch: Number(match.groups.patch), prerelease: match.groups.prerelease ? match.groups.prerelease.split(".").map((id) => (/^\d+$/.test(id) ? Number(id) : id)) : null, }; }; const comparePrereleaseIdentifiers = ( left: ReadonlyArray<string | number> | null, right: ReadonlyArray<string | number> | null, ): number => { if (left === null && right === null) return 0; if (left === null) return 1; if (right === null) return -1; const max = Math.max(left.length, right.length); for (let i = 0; i < max; i++) { const l = left[i]; const r = right[i]; if (l === undefined) return -1; if (r === undefined) return 1; if (l === r) continue; if (typeof l === "number" && typeof r === "number") return l < r ? -1 : 1; if (typeof l === "number") return -1; if (typeof r === "number") return 1; return l < r ? -1 : 1; } return 0; }; const compareVersions = (left: string, right: string): number | null => { const lv = parseVersion(left); const rv = parseVersion(right); if (!lv || !rv) return null; if (lv.major !== rv.major) return lv.major < rv.major ? -1 : 1; if (lv.minor !== rv.minor) return lv.minor < rv.minor ? -1 : 1; if (lv.patch !== rv.patch) return lv.patch < rv.patch ? -1 : 1; return comparePrereleaseIdentifiers(lv.prerelease, rv.prerelease); }; // ── useLatestVersion ──────────────────────────────────────────────────── function useLatestVersion(currentVersion: string) { const channel = resolveUpdateChannel(currentVersion); const [latestVersion, setLatestVersion] = useState<string | null>(null); useEffect(() => { let cancelled = false; void Effect.runPromiseExit( Effect.tryPromise({ try: async () => { const res = await fetch(EXECUTOR_DIST_TAGS_PATH); if (!res.ok) return null; return (await res.json()) as Partial<Record<UpdateChannel, string>>; }, catch: (cause) => cause, }), ).then((exit) => { if (!cancelled && Exit.isSuccess(exit)) { setLatestVersion(exit.value?.[channel] ?? null); } }); return () => { cancelled = true; }; }, [channel]); const updateAvailable = latestVersion !== null && compareVersions(currentVersion, latestVersion) === -1; return { latestVersion, updateAvailable, channel }; } // ── UpdateCard ────────────────────────────────────────────────────────── function UpdateCard(props: { latestVersion: string; channel: UpdateChannel }) { const command = `npm i -g executor@${props.channel}`; const [copied, setCopied] = useState(false); const handleCopy = useCallback(() => { void copyToClipboard(command).then((ok) => { if (!ok) { toast.error("Failed to copy to clipboard"); return; } setCopied(true); setTimeout(() => setCopied(false), 1500); }); }, [command]); return ( <div className="mx-2 mb-2 rounded-xl border border-primary/25 bg-primary/[0.06] p-3"> <div className="flex items-center gap-2"> <div className="flex size-5 shrink-0 items-center justify-center rounded-full bg-primary/15"> <svg viewBox="0 0 16 16" fill="none" className="size-3 text-primary"> <path d="M8 3v7M5 7l3 3 3-3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" /> <path d="M3 12h10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" /> </svg> </div> <div className="min-w-0"> <p className="text-xs font-semibold text-foreground">Update available</p> <p className="text-sm text-muted-foreground">v{props.latestVersion}</p> </div> </div> <Button type="button" variant="outline" onClick={handleCopy} className="mt-2.5 flex w-full items-center justify-between gap-2 rounded-lg border-border/60 bg-background/50 px-2.5 py-1.5 text-left hover:bg-background/80" > <code className="truncate font-mono text-xs text-sidebar-foreground">{command}</code> <span className="shrink-0 text-muted-foreground transition-colors group-hover:text-foreground"> {copied ? ( <svg viewBox="0 0 16 16" fill="none" className="size-3 text-primary"> <path d="M3 8.5l3.5 3.5L13 4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" /> </svg> ) : ( <svg viewBox="0 0 16 16" fill="none" className="size-3"> <rect x="5" y="5" width="8" height="8" rx="1.5" stroke="currentColor" strokeWidth="1.2" /> <path d="M3 11V3.5A.5.5 0 013.5 3H11" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" /> </svg> )} </span> </Button> </div> ); } // ── NavItem ────────────────────────────────────────────────────────────── function NavItem(props: { to: string; label: string; active: boolean; onNavigate?: () => void }) { return ( <Link to={props.to} onClick={props.onNavigate} className={[ "flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors", props.active ? "bg-sidebar-active text-foreground font-medium" : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground", ].join(" ")} > {props.label} </Link> ); } // ── 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) => ( <Link key={entry.key} to="/{-$orgSlug}/plugins/$pluginId/$" params={{ pluginId: entry.pluginId, _splat: entry.splat }} onClick={props.onNavigate} className={[ "flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors", props.pathname === entry.href || props.pathname.startsWith(`${entry.href}/`) ? "bg-sidebar-active text-foreground font-medium" : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground", ].join(" ")} > {entry.label} </Link> ))} </> ); } // ── IntegrationList ─────────────────────────────────────────────────────────── function IntegrationList(props: { pathname: string; onNavigate?: () => void }) { const integrations = useAtomValue(integrationsOptimisticAtom); const integrationPlugins = useIntegrationPlugins(); return AsyncResult.match(integrations, { onInitial: () => <div className="px-2.5 py-2 text-xs text-muted-foreground">Loading…</div>, onFailure: () => ( <div className="px-2.5 py-2 text-xs text-muted-foreground">No integrations yet</div> ), onSuccess: ({ value }: { readonly value: readonly Integration[] }) => value.length === 0 ? ( <div className="px-2.5 py-2 text-sm leading-relaxed text-muted-foreground"> No integrations yet </div> ) : ( <div className="flex flex-col gap-px"> {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 ( <Link key={slug} to="/{-$orgSlug}/integrations/$namespace" params={{ namespace: slug }} onClick={props.onNavigate} className={[ "group flex items-center gap-2 rounded-md px-2.5 py-1.5 text-xs transition-colors", active ? "bg-sidebar-active text-foreground font-medium" : "text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground", ].join(" ")} > <IntegrationIconWithAccount icon={integrationPresetIconUrl( { id: slug, kind: integration.kind }, integrationPlugins, )} sourceId={slug} size="sm" /> <span className="flex-1 truncate">{name}</span> <span className="rounded bg-secondary/50 px-1 py-px text-xs font-medium text-muted-foreground"> {integration.kind} </span> </Link> ); })} </div> ), }); } // ── SidebarContent ─────────────────────────────────────────────────────── function SidebarContent(props: { pathname: string; onNavigate?: () => void; showBrand?: boolean; updateAvailable: boolean; latestVersion: string | null; channel: UpdateChannel; }) { const isHome = props.pathname === "/"; const isSecrets = props.pathname === "/secrets"; const isPolicies = props.pathname === "/policies"; return ( <> {props.showBrand !== false && ( <div className="flex h-12 shrink-0 items-center gap-2 border-b border-sidebar-border px-4"> <Link to="/{-$orgSlug}" className="flex shrink-0 items-center gap-1.5"> <span className="font-display text-base tracking-tight text-foreground">executor</span> <span className="rounded bg-primary/15 px-1.5 py-px text-[10px] font-semibold uppercase tracking-wider text-primary"> Beta </span> </Link> <div className="ml-auto flex min-w-0 flex-1 justify-end"> <ServerConnectionMenu variant="header" /> </div> </div> )} <nav className="flex flex-1 flex-col overflow-y-auto p-2"> <NavItem to="/{-$orgSlug}" label="Integrations" active={isHome} onNavigate={props.onNavigate} /> <NavItem to="/{-$orgSlug}/secrets" label="Secrets" active={isSecrets} onNavigate={props.onNavigate} /> <NavItem to="/{-$orgSlug}/policies" label="Policies" active={isPolicies} onNavigate={props.onNavigate} /> <PluginNav pathname={props.pathname} onNavigate={props.onNavigate} /> {/* Sources list */} <div className="mt-5 mb-1 px-2.5 text-xs font-medium uppercase tracking-widest text-muted-foreground"> <span>Integrations</span> </div> <IntegrationList pathname={props.pathname} onNavigate={props.onNavigate} /> </nav> {props.updateAvailable && props.latestVersion && ( <UpdateCard latestVersion={props.latestVersion} channel={props.channel} /> )} {/* Footer */} <div className="shrink-0 border-t border-sidebar-border px-4 py-2.5"> <div className="flex flex-col gap-1.5 text-xs leading-none"> <a href={`${VITE_GITHUB_URL}/issues`} target="_blank" rel="noreferrer" className="text-muted-foreground transition-colors hover:text-foreground" > Feedback / bug? </a> <a href={VITE_GITHUB_URL} target="_blank" rel="noreferrer" className="text-muted-foreground transition-colors hover:text-foreground" > Star on GitHub </a> <span className="mt-0.5 text-xs text-muted-foreground tabular-nums"> v{VITE_APP_VERSION} </span> </div> </div> </> ); } // ── Shell ───────────────────────────────────────────────────────────────── export function Shell() { const location = useLocation(); const pathname = location.pathname; const refreshSources = useAtomRefresh(integrationsAtom); const refreshTools = useAtomRefresh(toolsAllAtom); const { latestVersion, updateAvailable, channel } = useLatestVersion(VITE_APP_VERSION); const lastPathname = useRef(pathname); const [mobileSidebarOpen, setMobileSidebarOpen] = 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 = () => { refreshSources(); refreshTools(); }; import.meta.hot.on("executor:backend-updated", refreshBackendData); return () => { import.meta.hot?.off("executor:backend-updated", refreshBackendData); }; }, [refreshSources, refreshTools]); return ( <div className="flex h-screen overflow-hidden"> <CommandPalette /> {/* Desktop sidebar */} <aside className="hidden w-52 shrink-0 border-r border-sidebar-border bg-sidebar md:flex md:flex-col lg:w-56"> <SidebarContent pathname={pathname} updateAvailable={updateAvailable} latestVersion={latestVersion} channel={channel} /> </aside> {/* Mobile sidebar overlay */} {mobileSidebarOpen && ( <div className="fixed inset-0 z-50 flex md:hidden"> {/* oxlint-disable-next-line react/forbid-elements */} <button type="button" aria-label="Close navigation" className="absolute inset-0 bg-black/45 backdrop-blur-[1px]" onClick={() => setMobileSidebarOpen(false)} /> <div className="relative flex h-full w-[84vw] max-w-xs flex-col border-r border-sidebar-border bg-sidebar shadow-2xl"> <div className="flex h-12 shrink-0 items-center justify-between border-b border-sidebar-border px-4"> <Link to="/{-$orgSlug}" className="flex items-center gap-1.5"> <span className="font-display text-base tracking-tight text-foreground"> executor </span> <span className="rounded bg-primary/15 px-1.5 py-px text-[10px] font-semibold uppercase tracking-wider text-primary"> Beta </span>