"use client"; import { useCallback, useMemo, useRef, useState } from "react"; import { Schema } from "effect"; import { PluginRuntimeResponseSchema, type PluginRuntimeView, } from "@local-studio/agent-runtime/plugin-runtime-contract"; import { ApiErrorResponseSchema } from "@local-studio/agent-runtime/api-contract"; import { Alert, Button, ModelButton, SearchInput, StatusPill, type UiTone, UiModal, UiModalBody, UiModalFooter, UiModalHeader, } from "@/ui"; import { Eye, X } from "@/ui/icon-registry"; import { ResourceDrawer, ResourceDrawerSection, ResourceFact } from "@/ui/resource-drawer"; import { ResourceLogo } from "@/ui/resource-logo"; import { useMountSubscription } from "@/hooks/use-mount-subscription"; import { SettingsButton, SettingsGroup } from "@/features/settings/settings-ui"; import { DataRow, EndCell, HeadCell, IdentityCell, RowAction, StatusText, statusToneFor, TableFrame, TableNotice, TableSection, TableSkeleton, TextCell, } from "@/features/recipes/recipes-content/catalog-table-shell"; import { GoogleAccountModal } from "./google-account-modal"; type PluginStatus = { label: string; tone: UiTone }; function responseError(body: unknown, fallback: string): string { try { return Schema.decodeUnknownSync(ApiErrorResponseSchema)(body).error; } catch { return fallback; } } async function pluginResponse(response: Response, fallback: string) { const body: unknown = await response.json().catch(() => null); if (!response.ok) throw new Error(responseError(body, fallback)); return Schema.decodeUnknownSync(PluginRuntimeResponseSchema)(body); } function capabilitySummary(plugin: PluginRuntimeView): string { return [ plugin.provides.skills ? "skills" : null, plugin.provides.mcpServers || plugin.account ? `${plugin.tools.serverCount} ${plugin.account ? "remote " : ""}MCP ${plugin.tools.serverCount === 1 ? "server" : "servers"}` : null, plugin.provides.apps ? "account app" : null, `v${plugin.version}`, ] .filter((value): value is string => Boolean(value)) .join(" · "); } function pluginStatus(plugin: PluginRuntimeView): PluginStatus { if (plugin.account && !plugin.account.configured) return { label: "Setup", tone: "warning" }; if (plugin.account && !plugin.account.connected) return { label: "Sign in", tone: "warning" }; if (plugin.tools.state === "enabled") { return { label: `Observe · ${plugin.tools.allowedToolCount} ${plugin.tools.allowedToolCount === 1 ? "tool" : "tools"}`, tone: "good", }; } if (plugin.tools.state === "available") return { label: "Available", tone: "info" }; if (plugin.tools.state === "disabled") return { label: "Off", tone: "default" }; if (plugin.tools.state === "invalid") return { label: "Unavailable", tone: "danger" }; if (plugin.tools.state === "configuration_required" || plugin.provides.apps) { return { label: "Adapter needed", tone: "warning" }; } return { label: "Skills", tone: "default" }; } function activationAction(plugin: PluginRuntimeView): "account" | "connect" | "disconnect" | null { if (plugin.account && !plugin.account.connected) return "account"; if (plugin.account) { return plugin.tools.state === "available" || plugin.tools.state === "disabled" ? "connect" : null; } if (plugin.tools.state === "enabled") return "disconnect"; if (plugin.tools.state === "available" || plugin.tools.state === "disabled") return "connect"; return null; } type PluginRowAction = ReturnType; function pluginActionLabel(plugin: PluginRuntimeView, action: PluginRowAction): string { if (action === "account") return plugin.account?.configured ? "Sign in" : "Set up"; if (action === "connect") return "Connect"; return "Disconnect"; } function PluginRowActions({ plugin, action, busy, onConnect, onDisconnect, onAccount, }: { plugin: PluginRuntimeView; action: PluginRowAction; busy: boolean; onConnect: () => void; onDisconnect: () => void; onAccount: () => void; }) { const actionLabel = action ? pluginActionLabel(plugin, action) : ""; const handleAction = action === "account" ? onAccount : action === "connect" ? onConnect : onDisconnect; return ( <> {plugin.account?.connected ? ( Manage ) : null} {action ? ( {busy ? "Working" : actionLabel} ) : null} ); } const PLUGIN_COLUMNS = ["Plugin", "Capabilities", "State"] as const; const PLUGIN_MIN_WIDTH = "min-w-[40rem]"; function PluginRow({ plugin, busy, onOpen, onConnect, onDisconnect, onAccount, }: { plugin: PluginRuntimeView; busy: boolean; onOpen: () => void; onConnect: () => void; onDisconnect: () => void; onAccount: () => void; }) { const status = pluginStatus(plugin); const action = activationAction(plugin); return ( } label={plugin.displayName} description={plugin.account?.email || plugin.description || plugin.category} /> {`${plugin.source} · ${capabilitySummary(plugin)}`}
{status.label} {action || plugin.account?.connected ? ( ) : null}
); } function PluginDrawer({ plugin, busy, onClose, onConnect, onDisconnect, onAccount, }: { plugin: PluginRuntimeView; busy: boolean; onClose: () => void; onConnect: () => void; onDisconnect: () => void; onAccount: () => void; }) { const status = pluginStatus(plugin); const action = activationAction(plugin); const capabilities = [ ...plugin.capabilities, plugin.provides.skills ? "Skills" : null, plugin.provides.mcpServers ? "MCP tools" : null, plugin.provides.apps ? "App integration" : null, ].filter((value): value is string => Boolean(value)); return ( } badge={{status.label}} status={`${plugin.source} · ${plugin.category} · v${plugin.version}`} footer={ action || plugin.account?.connected ? ( ) : null } onClose={onClose} >

{plugin.description || "No plugin description was provided."}

{plugin.account ? ( {plugin.account.email ? ( ) : null} ) : null}
); } export function PluginsSection() { const [plugins, setPlugins] = useState([]); const [loaded, setLoaded] = useState(false); const [error, setError] = useState(""); const [query, setQuery] = useState(""); const [busyId, setBusyId] = useState(null); const [pending, setPending] = useState(null); const [selectedPlugin, setSelectedPlugin] = useState(null); const [accountPlugin, setAccountPlugin] = useState(null); const requestGeneration = useRef(0); const loadPlugins = useCallback(() => { const generation = ++requestGeneration.current; return fetch("/api/agent/plugins", { cache: "no-store" }) .then(async (response) => { const payload = await pluginResponse(response, "Plugin discovery failed"); if (generation !== requestGeneration.current) return; setPlugins(payload.plugins); setError(""); }) .catch((loadError: unknown) => { if (generation !== requestGeneration.current) return; setError(loadError instanceof Error ? loadError.message : "Plugin discovery failed"); }) .finally(() => { if (generation === requestGeneration.current) setLoaded(true); }); }, []); useMountSubscription(() => { void loadPlugins(); }, [loadPlugins]); const handleAccountChanged = useCallback(() => { void loadPlugins(); }, [loadPlugins]); const visiblePlugins = useMemo(() => { const normalized = query.trim().toLowerCase(); if (!normalized) return plugins; return plugins.filter((plugin) => `${plugin.displayName} ${plugin.description} ${plugin.category} ${capabilitySummary(plugin)}` .toLowerCase() .includes(normalized), ); }, [plugins, query]); const setEnabled = async (plugin: PluginRuntimeView, enabled: boolean) => { const generation = ++requestGeneration.current; setBusyId(plugin.id); setError(""); try { const response = await fetch(`/api/agent/plugins/${encodeURIComponent(plugin.id)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enabled }), }); const payload = await pluginResponse(response, "Plugin activation failed"); if (generation !== requestGeneration.current) return; setPlugins(payload.plugins); setPending(null); } catch (activationError) { if (generation !== requestGeneration.current) return; setError( activationError instanceof Error ? activationError.message : "Plugin activation failed", ); } finally { setBusyId((current) => (current === plugin.id ? null : current)); } }; return ( <> {error ? (
{error}
) : null} {loaded ? `${visiblePlugins.length} of ${plugins.length}` : "discovering"} } > {!loaded ? ( ) : visiblePlugins.length === 0 ? ( ) : ( {PLUGIN_COLUMNS.map((column, index) => ( {column} ))} {visiblePlugins.map((plugin) => ( setSelectedPlugin(plugin)} onConnect={() => { setSelectedPlugin(null); setPending(plugin); }} onDisconnect={() => { setSelectedPlugin(null); void setEnabled(plugin, false); }} onAccount={() => { setSelectedPlugin(null); setAccountPlugin(plugin); }} /> ))} )} {selectedPlugin ? ( setSelectedPlugin(null)} onConnect={() => { setSelectedPlugin(null); setPending(selectedPlugin); }} onDisconnect={() => { setSelectedPlugin(null); void setEnabled(selectedPlugin, false); }} onAccount={() => { setSelectedPlugin(null); setAccountPlugin(selectedPlugin); }} /> ) : null} !busyId && setPending(null)} maxWidth="max-w-md" > } onClose={() => !busyId && setPending(null)} closeIcon={} /> Observe mode starts this plugin locally and exposes only tools it declares read-only. Desktop actions stay blocked until Local Studio has an action-time approval prompt.

The bundle remains in its installed location. Disconnecting stops exposing its tools to Workbench sessions.

{accountPlugin?.account?.provider === "google" ? ( setAccountPlugin(null)} onChanged={handleAccountChanged} /> ) : null} ); }