import { scopeThreadRef } from "@t3tools/client-runtime/environment"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { type EnvironmentId, type ThreadId, type ThreadSpecialist, type ThreadSpecialistMcpServer, specialistMcpServerEntries, withoutSpecialistMcpServer, withoutSpecialistSkill, } from "@t3tools/contracts"; import { BookOpenIcon, PlugIcon, XIcon } from "lucide-react"; import { useState, type ReactNode } from "react"; import { cn } from "../../lib/utils"; import { useThreadProjection } from "../../state/entities"; import { threadEnvironment } from "../../state/threads"; import { useOrchestrationCommand } from "../../state/use-orchestration-command"; import { stackedThreadToast, toastManager } from "../ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { ThreadDetailsControl } from "./ThreadDetailsControl"; import { ThreadDetailsSection } from "./ThreadDetailsSection"; import { THREAD_DETAILS_PANEL_ICON_CLASS } from "./threadDetailsPanelStyles"; interface ThreadSpecialistLoadoutPanelProps { readonly environmentId: EnvironmentId; readonly threadId: ThreadId; } const roleSource = (specialist: ThreadSpecialist) => `From role · @${specialist.key}`; const mcpSourceDetail = (server: ThreadSpecialistMcpServer) => server.type === "http" ? server.url : [server.command, ...(server.args ?? [])].join(" "); /** Role-scoped skills and MCP from the durable specialist binding. Hidden while unbound or empty. */ export function ThreadSpecialistLoadoutPanel(props: ThreadSpecialistLoadoutPanelProps) { const projection = useThreadProjection(scopeThreadRef(props.environmentId, props.threadId)); const specialist = projection?.projection.thread.specialist; const skills = specialist?.skills ?? []; const mcpEntries = specialistMcpServerEntries(specialist); const setSpecialist = useOrchestrationCommand(threadEnvironment.setSpecialist, { label: "thread role loadout", }); const [busyKey, setBusyKey] = useState(null); if (specialist === undefined || (skills.length === 0 && mcpEntries.length === 0)) { return null; } const reportFailure = (title: string, error: unknown) => { toastManager.add( stackedThreadToast({ type: "error", title, description: error instanceof Error ? error.message : String(error), }), ); }; const patch = async (next: ThreadSpecialist, busy: string) => { if (busyKey !== null) return; setBusyKey(busy); const result = await setSpecialist({ environmentId: props.environmentId, input: { threadId: props.threadId, specialist: next }, }); setBusyKey(null); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { reportFailure("Could not update role tools", squashAtomCommandFailure(result)); } }; return ( ); } function LoadoutRow(props: { readonly icon: ReactNode; readonly title: string; readonly source: string; readonly detail: string; readonly removeLabel: string; readonly busy: boolean; readonly onRemove: () => void; }) { return (
  • } > {props.icon} {props.title} {props.source} {props.detail}
    } > Remove from role
  • ); }