"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { IconAlertTriangle, IconCheck, IconClipboard, IconDownload, IconExternalLink, IconPlus, IconTerminal2, } from "@tabler/icons-react"; import { Badge } from "@kandev/ui/badge"; import { Button } from "@kandev/ui/button"; import { Card, CardContent } from "@kandev/ui/card"; import { Separator } from "@kandev/ui/separator"; import { useAppStore } from "@/components/state-provider"; import { createCustomTUIAgent, installAgent, listAgentDiscovery, listAgents, listAvailableAgents, listInstallJobs, } from "@/lib/api"; import type { InstallJob } from "@/lib/api"; import { useAgentDiscovery } from "@/hooks/domains/settings/use-agent-discovery"; import { useAvailableAgents } from "@/hooks/domains/settings/use-available-agents"; import { AgentLogo } from "@/components/agent-logo"; import { AddTUIAgentDialog } from "@/components/settings/add-tui-agent-dialog"; import { HostShellDialog } from "@/components/settings/host-shell-dialog"; import { InstallAgentCard } from "@/components/settings/install-agent-card"; import { InstalledAgentCard } from "@/components/settings/installed-agent-card"; import { toAgentProfileOption } from "@/lib/state/slices/settings/types"; import type { AgentDiscovery, Agent, AvailableAgent, AgentProfile, ToolStatus, } from "@/lib/types/http"; function useCopyCommand() { const [copiedValue, setCopiedValue] = useState(null); const copy = useCallback(async (text: string) => { try { await navigator.clipboard.writeText(text); } catch { // fallback const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); } setCopiedValue(text); setTimeout(() => setCopiedValue(null), 2000); }, []); return { copiedValue, copy }; } function CopyButton({ text, copiedValue, onCopy, }: { text: string; copiedValue: string | null; onCopy: (text: string) => void; }) { const isCopied = copiedValue === text; return ( ); } function InstallCard({ agent, copiedValue, onCopy, job, onInstall, }: { agent: AvailableAgent; copiedValue: string | null; onCopy: (text: string) => void; job: InstallJob | undefined; onInstall: (name: string) => void; }) { return ( {agent.install_script} ) : null } /> ); } function ToolInstallCard({ tool, copiedValue, onCopy, }: { tool: ToolStatus; copiedValue: string | null; onCopy: (text: string) => void; }) { return (

{tool.display_name}

{tool.available && ( Installed )}
{tool.description &&

{tool.description}

} {!tool.available && tool.install_script && (
{tool.install_script}
)} {tool.info_url && ( {tool.info_url} )}
); } type ProfileListItemProps = { agent: Agent; profile: AgentProfile; }; function ProfileListItem({ agent, profile }: ProfileListItemProps) { const profilePath = `/settings/agents/${encodeURIComponent(agent.name)}/profiles/${profile.id}`; return (
{agent.profiles[0]?.agent_display_name ?? agent.name} {agent.supports_mcp && MCP} {profile.name}
); } type InstalledAgentsSectionProps = { installedAgents: AgentDiscovery[]; discoveryLoading: boolean; rescanning: boolean; savedAgentsByName: Map; resolveDisplayName: (name: string) => string; resolveCapabilityStatus: (name: string) => string | undefined; setTuiDialogOpen: (open: boolean) => void; handleRescan: () => Promise; }; function InstalledAgentsSection({ installedAgents, discoveryLoading, rescanning, savedAgentsByName, resolveDisplayName, resolveCapabilityStatus, setTuiDialogOpen, handleRescan, }: InstalledAgentsSectionProps) { const [shellOpen, setShellOpen] = useState(false); return (

Installed Agents

Agents detected on this machine are ready to configure.

{ // Rescan after the user closes the shell - they may have installed // an agent CLI or fixed an auth issue inside the terminal. void handleRescan(); }} /> {installedAgents.length === 0 && (
{discoveryLoading ? ( Scanning for installed agents... ) : ( <> No installed agents were detected. Install one below, then click Rescan. )}
)}
{installedAgents.map((agent: AgentDiscovery) => ( void handleRescan()} /> ))}
); } function SuggestInstallSection({ notInstalledAgents, tools, copiedValue, onCopy, installJobs, onInstall, }: { notInstalledAgents: AvailableAgent[]; tools: ToolStatus[]; copiedValue: string | null; onCopy: (text: string) => void; installJobs: Record; onInstall: (name: string) => void; }) { const notInstalledTools = tools.filter((t) => !t.available); if (notInstalledAgents.length === 0 && notInstalledTools.length === 0) return null; return (

Available to Install

Click Install to run the agent's install script on the kandev host. Progress streams live; you can start multiple installs in parallel.

{notInstalledAgents.map((agent) => ( ))} {notInstalledTools.map((tool) => ( ))}
); } type AgentProfilesSectionProps = { savedAgents: Agent[]; }; function AgentProfilesSection({ savedAgents }: AgentProfilesSectionProps) { if (!savedAgents.some((agent: Agent) => agent.profiles.length > 0)) { return null; } return (

Agent Profiles

Manage existing profiles by agent.

{savedAgents.flatMap((agent: Agent) => agent.profiles.map((profile: AgentProfile) => ( )), )}
); } /** * Install state is held in the store (driven by WS events * agent.install.{started,output,finished}). This hook: * - Rehydrates jobs on mount so a page reload picks up in-flight installs. * - Subscribes to agent.available.updated → calls onSuccess() to rescan. * - Exposes handleInstall(name) which POSTs to enqueue (idempotent on the * server: clicking again while running returns the same job_id). */ function useInstallAgent(onSuccess: () => Promise) { const installJobs = useAppStore((state) => state.installJobs.byAgent); const upsertInstallJob = useAppStore((state) => state.upsertInstallJob); useEffect(() => { let cancelled = false; listInstallJobs() .then((resp) => { if (cancelled) return; // Upsert per-job rather than wholesale-replace: if a WS event // already seeded an in-flight job with output chunks between page // mount and this HTTP response, the snapshot from the server may // be older, and a full replace would clobber the live output. for (const job of resp.jobs) upsertInstallJob(job); }) .catch(() => { /* page mount; ignore */ }); return () => { cancelled = true; }; }, [upsertInstallJob]); // When any install finishes successfully, trigger the page-level rescan so // the agent disappears from "Available to Install" and shows up under // "Installed Agents". useEffect(() => { const succeeded = Object.values(installJobs).filter((j) => j.status === "succeeded"); if (succeeded.length > 0) { void onSuccess(); } // Intentionally only depends on the count of succeeded jobs to avoid // re-firing on every output chunk during a running install. // eslint-disable-next-line react-hooks/exhaustive-deps }, [Object.values(installJobs).filter((j) => j.status === "succeeded").length]); const handleInstall = useCallback( async (name: string) => { try { const job = await installAgent(name); // The WS event will normally arrive first, but seed the store in case // the WS round-trip is slower than the HTTP response. upsertInstallJob(job); } catch (err) { upsertInstallJob({ job_id: `local-error-${name}`, agent_name: name, status: "failed", error: err instanceof Error ? err.message : String(err), started_at: new Date().toISOString(), }); } }, [upsertInstallJob], ); return { installJobs, handleInstall }; } function useAgentPageState() { const { items: discoveryAgents, loading: discoveryLoading } = useAgentDiscovery(); const savedAgents = useAppStore((state) => state.settingsAgents.items); const setAgentDiscovery = useAppStore((state) => state.setAgentDiscovery); const setSettingsAgents = useAppStore((state) => state.setSettingsAgents); const setAvailableAgents = useAppStore((state) => state.setAvailableAgents); const setAgentProfiles = useAppStore((state) => state.setAgentProfiles); const { items: availableAgents, tools } = useAvailableAgents(); const [rescanning, setRescanning] = useState(false); const [tuiDialogOpen, setTuiDialogOpen] = useState(false); const installedAgents = useMemo( () => discoveryAgents.filter((agent: AgentDiscovery) => agent.available), [discoveryAgents], ); const notInstalledAgents = useMemo( () => availableAgents.filter((a: AvailableAgent) => !a.available && a.install_script), [availableAgents], ); const savedAgentsByName = useMemo( () => new Map(savedAgents.map((agent: Agent) => [agent.name, agent])), [savedAgents], ); const resolveDisplayName = (name: string) => availableAgents.find((item: AvailableAgent) => item.name === name)?.display_name ?? name; const resolveCapabilityStatus = (name: string) => availableAgents.find((item: AvailableAgent) => item.name === name)?.model_config?.status; const handleRescan = async () => { if (rescanning) return; setRescanning(true); try { const [discoveryResp, availableResp] = await Promise.all([ listAgentDiscovery({ cache: "no-store" }), listAvailableAgents({ cache: "no-store" }), ]); setAgentDiscovery(discoveryResp.agents); setAvailableAgents(availableResp.agents, availableResp.tools ?? []); } finally { setRescanning(false); } }; const { installJobs, handleInstall } = useInstallAgent(handleRescan); const handleCreateCustomTUI = async (data: { display_name: string; model?: string; command: string; }) => { await createCustomTUIAgent(data); const [discoveryResp, agentsResp, availableResp] = await Promise.all([ listAgentDiscovery({ cache: "no-store" }), listAgents({ cache: "no-store" }), listAvailableAgents({ cache: "no-store" }), ]); setAgentDiscovery(discoveryResp.agents); setSettingsAgents(agentsResp.agents); setAgentProfiles( agentsResp.agents.flatMap((agent) => agent.profiles.map((profile) => toAgentProfileOption(agent, profile)), ), ); setAvailableAgents(availableResp.agents, availableResp.tools ?? []); }; return { savedAgents, installedAgents, notInstalledAgents, tools, savedAgentsByName, discoveryLoading, rescanning, tuiDialogOpen, setTuiDialogOpen, resolveDisplayName, resolveCapabilityStatus, handleRescan, handleCreateCustomTUI, installJobs, handleInstall, }; } export default function AgentsSettingsPage() { const { savedAgents, installedAgents, notInstalledAgents, tools, savedAgentsByName, discoveryLoading, rescanning, tuiDialogOpen, setTuiDialogOpen, resolveDisplayName, resolveCapabilityStatus, handleRescan, handleCreateCustomTUI, installJobs, handleInstall, } = useAgentPageState(); const { copiedValue, copy } = useCopyCommand(); return (

Agents

Discover installed agents, install new ones, and manage their profiles.

); }