import { useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate } from "@tanstack/react-router"; import { useAtomValue } from "@effect/atom-react"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import { PlusIcon } from "lucide-react"; import { SourceFavicon } from "./source-favicon"; import { sourcesOptimisticAtom } from "../api/atoms"; import { useScope } from "../hooks/use-scope"; import { useSourcePlugins } from "@executor-js/sdk/client"; import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, } from "./command"; // --------------------------------------------------------------------------- // CommandPalette — global ⌘K navigator. // // Order of entries: // 1. Connected sources (priority, shown first) // 2. Add actions for each available source plugin // 3. Popular sources (plugin presets) // --------------------------------------------------------------------------- export function CommandPalette() { const sourcePlugins = useSourcePlugins(); const [open, setOpen] = useState(false); const navigate = useNavigate(); const scopeId = useScope(); const sourcesResult = useAtomValue(sourcesOptimisticAtom(scopeId)); // Toggle with ⌘K / Ctrl+K useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setOpen((o) => !o); } }; document.addEventListener("keydown", onKeyDown); return () => document.removeEventListener("keydown", onKeyDown); }, []); const connectedSources = useMemo( () => AsyncResult.match(sourcesResult, { onInitial: () => [] as Array<{ id: string; name: string; kind: string; url?: string; runtime?: boolean; }>, onFailure: () => [] as Array<{ id: string; name: string; kind: string; url?: string; runtime?: boolean; }>, onSuccess: ({ value }) => value.filter((s: { readonly runtime?: boolean }) => !s.runtime), }), [sourcesResult], ); const presetEntries = useMemo(() => { const entries: Array<{ pluginKey: string; pluginLabel: string; presetId: string; presetName: string; presetSummary?: string; presetUrl?: string; presetIcon?: string; }> = []; for (const plugin of sourcePlugins) { for (const preset of plugin.presets ?? []) { entries.push({ pluginKey: plugin.key, pluginLabel: plugin.label, presetId: preset.id, presetName: preset.name, presetSummary: preset.summary, presetUrl: preset.url, presetIcon: preset.icon, }); } } return entries; }, [sourcePlugins]); const close = useCallback(() => setOpen(false), []); const goToSource = useCallback( (id: string) => { close(); void navigate({ to: "/sources/$namespace", params: { namespace: id } }); }, [close, navigate], ); const goToAdd = useCallback( (pluginKey: string) => { close(); void navigate({ to: "/sources/add/$pluginKey", params: { pluginKey }, }); }, [close, navigate], ); const goToPreset = useCallback( (pluginKey: string, presetId: string, presetUrl?: string) => { close(); const search: Record = { preset: presetId }; if (presetUrl) search.url = presetUrl; void navigate({ to: "/sources/add/$pluginKey", params: { pluginKey }, search, }); }, [close, navigate], ); return ( No results found. {connectedSources.length > 0 && ( {connectedSources.map( (s: { readonly id: string; readonly name: string; readonly kind: string; readonly url?: string; }) => ( goToSource(s.id)} > {s.name} {s.kind} ), )} )} {connectedSources.length > 0 && sourcePlugins.length > 0 && } {sourcePlugins.length > 0 && ( {sourcePlugins.map((plugin) => ( goToAdd(plugin.key)} > Add {plugin.label} ))} )} {presetEntries.length > 0 && } {presetEntries.length > 0 && ( {presetEntries.map((e) => ( goToPreset(e.pluginKey, e.presetId, e.presetUrl)} > {e.presetIcon ? ( ) : ( )} {e.presetName} {e.pluginLabel} ))} )} ); }