import { Suspense, useState, useCallback, useMemo } from "react"; import { Link, useNavigate } from "@tanstack/react-router"; import { Result, useAtomSet } from "@effect-atom/atom-react"; import { detectSource } from "../api/atoms"; import { useSourcesWithPending } from "../api/optimistic"; import { useScope } from "../hooks/use-scope"; import type { SourcePlugin, SourcePreset } from "../plugins/source-plugin"; import { McpInstallCard } from "../components/mcp-install-card"; import { Button } from "../components/button"; import { Badge } from "../components/badge"; import { Input } from "../components/input"; import { CardStack, CardStackHeader, CardStackContent, CardStackEntry, CardStackEntryField, CardStackEntryMedia, CardStackEntryContent, CardStackEntryTitle, CardStackEntryDescription, CardStackEntryActions, } from "../components/card-stack"; import { SourceFavicon } from "../components/source-favicon"; import { Skeleton } from "../components/skeleton"; const KIND_TO_PLUGIN_KEY: Record = { openapi: "openapi", mcp: "mcp", graphql: "graphql", googleDiscovery: "googleDiscovery", }; // --------------------------------------------------------------------------- // Page // --------------------------------------------------------------------------- export function SourcesPage(props: { sourcePlugins: readonly SourcePlugin[] }) { const { sourcePlugins } = props; const [url, setUrl] = useState(""); const [detecting, setDetecting] = useState(false); const [error, setError] = useState(null); const scopeId = useScope(); const sources = useSourcesWithPending(scopeId); const doDetect = useAtomSet(detectSource, { mode: "promise" }); const navigate = useNavigate(); const handleDetect = useCallback(async () => { const trimmed = url.trim(); if (!trimmed) return; setDetecting(true); setError(null); try { const results = await doDetect({ path: { scopeId }, payload: { url: trimmed }, }); if (results.length === 0) { setError("Could not detect a source type from this URL. Try adding manually."); setDetecting(false); return; } const pluginKey = KIND_TO_PLUGIN_KEY[results[0].kind]; if (pluginKey) { void navigate({ to: "/sources/add/$pluginKey", params: { pluginKey }, search: { url: trimmed, namespace: results[0].namespace }, }); } else { setError(`Detected source type "${results[0].kind}" but no plugin is available for it.`); } } catch { setError("Detection failed. Try adding a source manually."); } finally { setDetecting(false); } }, [url, doDetect, navigate, scopeId]); return (
{/* Header */}

Sources

Tool providers available in this workspace.

{/* URL detection input */}
{ setUrl((e.target as HTMLInputElement).value); setError(null); }} onKeyDown={(e) => { if (e.key === "Enter") handleDetect(); }} placeholder="https://..." disabled={detecting} className="flex-1" />
Or add manually:{" "} {sourcePlugins.map((p) => ( {p.label} ))}
{Result.match(sources, { onInitial: () => , onFailure: () =>

Failed to load sources

, onSuccess: ({ value }) => { const connectedSources = value.filter((source) => !source.runtime); return value.length === 0 ? (

No sources yet

Add a source to get started.

) : (
{connectedSources.length > 0 && (
)}
); }, })}
); } // --------------------------------------------------------------------------- // Preset grid // --------------------------------------------------------------------------- type PresetEntry = { preset: SourcePreset; pluginKey: string; pluginLabel: string; }; function PresetGrid(props: { plugins: readonly SourcePlugin[] }) { const allPresets = useMemo(() => { const entries: PresetEntry[] = []; for (const plugin of props.plugins) { for (const preset of plugin.presets ?? []) { entries.push({ preset, pluginKey: plugin.key, pluginLabel: plugin.label, }); } } return entries; }, [props.plugins]); if (allPresets.length === 0) return null; return (
Popular sources {allPresets.map(({ preset, pluginKey, pluginLabel }) => { const search: Record = { preset: preset.id }; if (preset.url) search.url = preset.url; return ( {preset.icon ? ( ) : ( )} {preset.name} {preset.summary} {pluginLabel} ); })}
); } // --------------------------------------------------------------------------- // Source grid // --------------------------------------------------------------------------- function SourceGrid(props: { sources: readonly { id: string; name: string; kind: string; url?: string; runtime?: boolean; }[]; sourcePlugins: readonly SourcePlugin[]; }) { const pluginByKind = useMemo(() => { const out = new Map(); for (const p of props.sourcePlugins) out.set(p.key, p); return out; }, [props.sourcePlugins]); return ( Connected {props.sources.map((s) => { const pluginKey = KIND_TO_PLUGIN_KEY[s.kind] ?? s.kind; const plugin = pluginByKind.get(pluginKey); const SummaryComponent = plugin?.summary; return ( {s.name} {s.id} {SummaryComponent && ( )} {s.runtime && built-in} {s.kind} ); })} ); } // --------------------------------------------------------------------------- // Loading skeleton // --------------------------------------------------------------------------- function SourcesGridSkeleton() { return ( {Array.from({ length: 4 }).map((_, i) => (
))}
); }