import { useActionMutation, useActionQuery, } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { IconCheck, IconCopy, IconPlugConnected } from "@tabler/icons-react"; import { useMemo, useState } from "react"; import { useLocation } from "react-router"; import { toast } from "sonner"; import { ActionQueryError } from "../../components/action-query-error"; import { AgentsPanel, type ConnectedAgent, } from "../../components/agents-panel"; import { DispatchShell } from "../../components/dispatch-shell"; import { SimpleAgentsPanel } from "../../components/simple-agents-panel"; import { Button } from "../../components/ui/button"; import { Input } from "../../components/ui/input"; import { Skeleton } from "../../components/ui/skeleton"; import { Switch } from "../../components/ui/switch"; export function meta() { return [{ title: "Agents — Dispatch" }]; } interface McpAccessApp { id: string; name: string; description: string; url: string; color: string; granted: boolean; } type McpAccessMode = "all-apps" | "selected-apps"; interface McpAccessState { mode: McpAccessMode; selectedAppIds: string[]; } function dispatchMcpUrl(): string { const path = "/mcp"; if (typeof window === "undefined") return path; return new URL(path, window.location.origin).href; } function DispatchMcpAccessPanel() { const t = useT(); const { data, isLoading, isError, error, refetch } = useActionQuery( "list-mcp-app-access", {}, ); const [optimistic, setOptimistic] = useState(null); const saveAccess = useActionMutation("set-mcp-app-access", { onSuccess: () => { setOptimistic(null); toast.success(t("dispatch.pages.mcpAccessUpdated")); }, onError: (error) => { setOptimistic(null); toast.error(error.message); }, }); const apps = ((data as { apps?: McpAccessApp[] } | undefined)?.apps ?? []) as McpAccessApp[]; const access = optimistic ?? ({ mode: ((data as { mode?: McpAccessMode } | undefined)?.mode ?? "all-apps") as McpAccessMode, selectedAppIds: (data as { selectedAppIds?: string[] } | undefined)?.selectedAppIds ?? [], } satisfies McpAccessState); const selected = useMemo( () => new Set(access.selectedAppIds), [access.selectedAppIds], ); const grantedCount = access.mode === "all-apps" ? apps.length : access.selectedAppIds.length; const mcpUrl = dispatchMcpUrl(); function persist(next: McpAccessState) { setOptimistic(next); saveAccess.mutate(next); } function toggleApp(appId: string) { const next = selected.has(appId) ? access.selectedAppIds.filter((id) => id !== appId) : [...access.selectedAppIds, appId]; persist({ mode: "selected-apps", selectedAppIds: next }); } async function copyUrl() { try { await navigator.clipboard.writeText(mcpUrl); toast.success(t("dispatch.pages.mcpUrlCopied")); } catch { toast.error(t("dispatch.pages.mcpUrlCopyFailed")); } } return (
{t("dispatch.pages.unifiedMcpGateway")}
{t("dispatch.pages.unifiedMcpGatewayDescription")}{" "} list_apps, ask_app, and{" "} open_app.
{access.mode === "all-apps" ? t("dispatch.pages.allApps") : t("dispatch.pages.selectedApps")}
{isLoading ? ( ) : ( t("dispatch.pages.grantedCount", { count: grantedCount }) )}
persist({ mode: checked ? "all-apps" : "selected-apps", selectedAppIds: checked ? access.selectedAppIds : apps.map((app) => app.id), }) } aria-label={t("dispatch.pages.exposeAllAppsMcp")} />
{isError ? ( void refetch()} /> ) : access.mode === "selected-apps" ? (
{apps.map((app) => { const isSelected = selected.has(app.id); return ( ); })}
) : null}
); } function ConnectedAgentsRoute() { const t = useT(); const agentsQuery = useActionQuery("list-connected-agents", {}); return (
{agentsQuery.isError ? ( void agentsQuery.refetch()} /> ) : ( )}
); } export default function AgentsRoute() { const t = useT(); const location = useLocation(); if (!location.pathname.startsWith("/admin/agents")) { return ( ); } return ; }