"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, SearchInput, StatusPill, UiModal, UiModalHeader } from "@/ui";
import { Eye, X } from "@/ui/icon-registry";
import { useMountSubscription } from "@/hooks/use-mount-subscription";
import {
SettingsButton,
SettingsGroup,
SettingsRow,
SettingsValue,
type StatusTone,
} from "@/features/settings/settings-ui";
import { GoogleAccountModal } from "./google-account-modal";
import { ChatterboxVoiceModal } from "./chatterbox-voice-modal";
import { speechStatusLabel, speechStatusTone } from "./chatterbox-voice-model";
import { useSpeechStore, type SpeechSnapshot } from "./chatterbox-voice-store";
type PluginStatus = { label: string; tone: StatusTone };
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 {
if (plugin.hostCapability?.capability === "speech") {
return `local speech · voice cloning · v${plugin.version}`;
}
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, speech: SpeechSnapshot): PluginStatus {
if (plugin.hostCapability?.capability === "speech") {
if (!speech.available && !speech.loading) return { label: "Unavailable", tone: "danger" };
if (speech.status) {
return {
label: speechStatusLabel(speech.status),
tone: speechStatusTone(speech.status),
};
}
if (speech.loading) return { label: "Checking", tone: "default" };
return { label: speech.error ? "Unavailable" : "Configure", tone: "warning" };
}
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: "Invalid manifest", 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.hostCapability) return 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;
}
function PluginRowsSkeleton() {
return (
<>
{[0, 1, 2].map((index) => (
))}
>
);
}
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,
hostActionLabel,
onConnect,
onDisconnect,
onAccount,
onHostCapability,
}: {
plugin: PluginRuntimeView;
action: PluginRowAction;
busy: boolean;
hostActionLabel: string;
onConnect: () => void;
onDisconnect: () => void;
onAccount: () => void;
onHostCapability: () => void;
}) {
const actionLabel = action ? pluginActionLabel(plugin, action) : "";
const handleAction =
action === "account" ? onAccount : action === "connect" ? onConnect : onDisconnect;
return (
<>
{plugin.hostCapability ? (
{hostActionLabel}
) : null}
{plugin.account?.connected ? (
Manage
) : null}
{action ? (
{busy ? "Working" : actionLabel}
) : null}
>
);
}
function PluginRow({
plugin,
speech,
busy,
onConnect,
onDisconnect,
onAccount,
onHostCapability,
}: {
plugin: PluginRuntimeView;
speech: SpeechSnapshot;
busy: boolean;
onConnect: () => void;
onDisconnect: () => void;
onAccount: () => void;
onHostCapability: () => void;
}) {
const status = pluginStatus(plugin, speech);
const action = activationAction(plugin);
const hostActionLabel = speech.status?.install.phase === "ready" ? "Manage" : "Configure";
return (
{capabilitySummary(plugin)}}
status={{status.label} }
actions={
action || plugin.account?.connected || plugin.hostCapability ? (
) : undefined
}
>
{plugin.tools.reason ? (
{plugin.tools.reason}
) : null}
{plugin.account?.email ? (
{plugin.account.email}
) : null}
);
}
export function PluginsSection() {
const speech = useSpeechStore();
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 [accountPlugin, setAccountPlugin] = useState(null);
const [speechPlugin, setSpeechPlugin] = 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 (
<>
Codex-compatible bundles discovered locally. Skills are available immediately; executable
tools require explicit permission.
{error ? (
) : null}
{loaded ? `${visiblePlugins.length} of ${plugins.length}` : "discovering"}
}
>
{!loaded ? (
) : visiblePlugins.length ? (
visiblePlugins.map((plugin) => (
setPending(plugin)}
onDisconnect={() => void setEnabled(plugin, false)}
onAccount={() => setAccountPlugin(plugin)}
onHostCapability={() => setSpeechPlugin(plugin)}
/>
))
) : (
{plugins.length ? `No plugins match “${query}”.` : "No plugin manifests found."}
)}
!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.
setPending(null)} disabled={Boolean(busyId)}>
Cancel
pending && void setEnabled(pending, true)}
disabled={!pending || Boolean(busyId)}
loading={Boolean(busyId)}
>
Connect in observe mode
{accountPlugin?.account?.provider === "google" ? (
setAccountPlugin(null)}
onChanged={handleAccountChanged}
/>
) : null}
{speechPlugin?.hostCapability?.capability === "speech" ? (
setSpeechPlugin(null)} />
) : null}
>
);
}