import { useEffect, useMemo, useState } from "react" import { useNavigate } from "react-router-dom" import { ExternalLink, Gauge, Maximize2, Search } from "lucide-react" import { useGateway, useGraph, useMeshDeployments, usePrograms } from "@/services/api/hooks" import { kindIcon } from "@/lib/labels" const ProgramIcon = kindIcon("program") // The command palette — the keyboard twin of the System map's inspect panel. ⌘K // from anywhere: an empty query is the "Start Menu" (launchable apps); typing // searches every deployment and program. One row per real thing: a deployed // program is a single row (with a 📦 jump to its source page); a source-only // program — one with no deployment — gets its own row. Enter launches (or, for // non-launchable, jumps to it in the System map, else opens its detail page). interface AppItem { id: string name: string kind: string machine: string | null // remote hostname, or null for local launchUrl?: string detailPath: string mapNodeId?: string // set for graph/mesh nodes; absent for catalog-only programs programPath?: string // the source/catalog page behind a deployment (/programs/) } function detailPathFor(kind: string, name: string): string { if (kind === "job") return `/jobs/${name}` if (kind === "tool") return `/tools/${name}` if (kind === "reference") return `/system` return `/services/${name}` } // Parent: owns only the open state + the global ⌘K / event triggers. The body // mounts fresh each open (so query/selection reset for free). export function CommandPalette() { const [open, setOpen] = useState(false) useEffect(() => { const onKey = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault() setOpen((o) => !o) } } const onOpen = () => setOpen(true) window.addEventListener("keydown", onKey) window.addEventListener("open-command-palette", onOpen) return () => { window.removeEventListener("keydown", onKey) window.removeEventListener("open-command-palette", onOpen) } }, []) if (!open) return null return setOpen(false)} /> } function PaletteBody({ onClose }: { onClose: () => void }) { const navigate = useNavigate() const { data: graph } = useGraph() const { data: gateway } = useGateway() const { data: mesh } = useMeshDeployments() const { data: programs } = usePrograms() const [query, setQuery] = useState("") const [sel, setSel] = useState(0) const apps = useMemo(() => { const domain = gateway?.domain const out: AppItem[] = [] // Programs already represented by a local deployment row — so we don't list // them a second time as standalone catalog entries. const deployedPrograms = new Set( (graph?.nodes ?? []).map((n) => n.program).filter((p): p is string => !!p), ) for (const n of graph?.nodes ?? []) { const launchUrl = n.kind === "reference" ? (n.base_url ?? undefined) : domain && n.reach && n.reach !== "off" && (n.kind === "static" || (n.kind === "service" && n.endpoints.some((e) => e.protocol === "http"))) ? `https://${n.name}.${domain}` : undefined out.push({ id: n.name, name: n.name, kind: n.kind, machine: null, launchUrl, detailPath: detailPathFor(n.kind, n.name), mapNodeId: n.name, programPath: n.program ? `/programs/${n.program}` : undefined, }) } for (const md of mesh?.deployments ?? []) { // A remote app is launchable at . when the node has an // acme domain and the app is http-exposed (subdomain set); references carry base_url. const remoteLaunch = md.kind === "reference" ? (md.base_url ?? undefined) : md.subdomain && md.domain ? `https://${md.subdomain}.${md.domain}` : undefined out.push({ id: `${md.node}/${md.name}`, name: md.name, kind: md.kind, machine: md.node, launchUrl: remoteLaunch, detailPath: `/node/${md.node}`, mapNodeId: `__remote_${md.node}_${md.name}__`, }) } // Source-only programs — those with no deployment on this node. A deployed // program is already reachable via its deployment row's 📦 icon, so it isn't // repeated here. Not launchable, so these surface only when the user types // (never in the empty "Start Menu"); Enter opens the program detail page. for (const p of programs ?? []) { if (deployedPrograms.has(p.id)) continue out.push({ id: `program:${p.id}`, name: p.id, kind: "program", machine: null, detailPath: `/programs/${p.id}`, }) } return out }, [graph, gateway, mesh, programs]) const results = useMemo(() => { const q = query.trim().toLowerCase() // Empty query = the Start Menu: just the launchable apps, browsable. if (!q) return apps.filter((a) => a.launchUrl) return apps.filter((a) => a.name.toLowerCase().includes(q) || a.kind.includes(q)) }, [apps, query]) const launch = (a: AppItem) => { onClose() if (a.launchUrl) window.open(a.launchUrl, "_blank", "noreferrer") else if (a.mapNodeId) navigate(`/system?focus=${encodeURIComponent(a.mapNodeId)}`) else navigate(a.detailPath) } const goToMap = (a: AppItem) => { if (!a.mapNodeId) return onClose() navigate(`/system?focus=${encodeURIComponent(a.mapNodeId)}`) } const details = (a: AppItem) => { onClose() navigate(a.detailPath) } const goToProgram = (a: AppItem) => { if (!a.programPath) return onClose() navigate(a.programPath) } return (
e.stopPropagation()} >
{ setQuery(e.target.value) setSel(0) }} onKeyDown={(e) => { if (e.key === "ArrowDown") { e.preventDefault() setSel((s) => Math.min(s + 1, results.length - 1)) } else if (e.key === "ArrowUp") { e.preventDefault() setSel((s) => Math.max(s - 1, 0)) } else if (e.key === "Enter") { e.preventDefault() const r = results[sel] if (r) launch(r) } else if (e.key === "Escape") { onClose() } }} placeholder="Launch or find anything…" className="flex-1 bg-transparent py-3 text-sm text-[var(--foreground)] outline-none placeholder:text-[var(--muted)]" /> esc
{results.length === 0 && (
No matches.
)} {results.map((a, i) => { const KindIcon = kindIcon(a.kind) return (
setSel(i)} onClick={() => launch(a)} className={`flex cursor-pointer items-center gap-2 px-3 py-2 text-sm ${i === sel ? "bg-white/10" : ""}`} > {a.name} {a.machine && ( {a.machine} )} {a.kind} {a.launchUrl && ( )} {a.mapNodeId && ( )} {a.programPath && ( )}
) })}
↑↓ navigate · ↵ launch/open · esc close {results.length} {query ? "matches" : "apps"}
) }