Skip to main content
Glama
AgentProfiles.tsx13 kB
import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { AgentProfile, DEFAULT_PROFILE_NAME, ToolGroup } from "@/store"; import sortBy from "lodash/sortBy"; import { CopyPlus, Info, Plus, Trash2 } from "lucide-react"; import { useState } from "react"; import { MultiSelect } from "./MultiSelect"; import { MultiSelectTools } from "./MultiSelectTools"; import { ToolGroupModal } from "./ToolGroupModal"; const AgentProfileRow = ({ agentsOptions, isDefaultProfile = false, isPendingUpdateAppConfig, handleProfileChange, onCreateNewAgent, onCreateNewToolGroup, onDuplicateProfile, onRemoveProfile, profile, toolGroups, }: { agentsOptions: { label: string; value: string; disabled?: boolean }[]; isDefaultProfile?: boolean; isPendingUpdateAppConfig: boolean; handleProfileChange: ( id: string, field: keyof AgentProfile, value: AgentProfile[keyof AgentProfile], ) => void; onCreateNewAgent: (newAgentName: string) => void; onCreateNewToolGroup: () => void; onDuplicateProfile: (profileId: string) => void; onRemoveProfile: (profileId: string) => void; profile: AgentProfile; toolGroups: ToolGroup[]; }) => { const [focused, setFocused] = useState(false); const handleFocus = () => setFocused(true); const handleBlur = () => setFocused(false); const shouldAutoFocus = focused && !isDefaultProfile; const enableRemoveButton = !isDefaultProfile; const enableDuplicateButton = !isDefaultProfile && profile.name !== ""; if (!profile) { return null; } const handleAgentsChange = (agent: string) => { const currentAgents = profile.agents || []; const newAgents = currentAgents.includes(agent) ? currentAgents.filter((a) => a !== agent) : [...currentAgents, agent]; handleProfileChange( profile.id, "agents", sortBy(newAgents, (a) => a.toLowerCase()), ); }; const handleToolGroupsChange = (toolGroup: string) => { const currentToolGroups = profile.toolGroups || []; const newToolGroups = currentToolGroups.includes(toolGroup) ? currentToolGroups.filter((tg) => tg !== toolGroup) : [...currentToolGroups, toolGroup]; handleProfileChange( profile.id, "toolGroups", sortBy(newToolGroups, (tg) => tg.toLowerCase()), ); }; return ( <tr className="border-t border-[var(--color-border-primary)]"> <td className="p-2 w-64"> <Input value={profile.name} onChange={(e) => handleProfileChange(profile.id, "name", e.target.value) } placeholder="Profile Name" className="bg-background shadow-none rounded-md border-[1px] font-normal" disabled={isDefaultProfile || isPendingUpdateAppConfig} autoFocus={shouldAutoFocus} onFocus={handleFocus} onBlur={handleBlur} /> </td> <td className="p-2 w-64"> <MultiSelect options={agentsOptions} selected={profile.agents} onCreateNew={onCreateNewAgent} onSelectionChange={handleAgentsChange} getTriggerText={(selected) => isDefaultProfile ? "any agent" : selected.length > 0 ? `${selected.length} selected` : `Select Agents...` } disabled={isDefaultProfile || isPendingUpdateAppConfig} /> </td> <td className="p-2 w-64"> <Select value={profile.permission} onValueChange={(value) => handleProfileChange(profile.id, "permission", value) } disabled={isPendingUpdateAppConfig} > <SelectTrigger className="bg-background"> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="allow">Allow</SelectItem> <SelectItem value="allow-all">Allow All</SelectItem> <SelectItem value="block">Block</SelectItem> <SelectItem value="block-all">Block All</SelectItem> </SelectContent> </Select> </td> <td className="p-2 w-64"> <MultiSelectTools title="Tool Groups" options={toolGroups} selected={profile.toolGroups} onSelectionChange={handleToolGroupsChange} onCreateNew={onCreateNewToolGroup} disabled={ isPendingUpdateAppConfig || profile.permission === "allow-all" || profile.permission === "block-all" } placeholder={ profile.permission === "allow-all" || profile.permission === "block-all" ? "All Tools" : undefined } /> </td> <td className="p-2 flex items-center justify-end gap-2"> {enableDuplicateButton && ( <Button onClick={() => onDuplicateProfile(profile.id)} size="icon" variant="secondary" className="text-[var(--color-fg-interactive)] hover:text-[--color-fg-interactive-hover]" disabled={isPendingUpdateAppConfig} > <CopyPlus className="w-4 h-4" /> </Button> )} {enableRemoveButton && ( <Button onClick={() => onRemoveProfile(profile.id)} size="icon" variant="secondary" className="text-[var(--color-fg-interactive)]" disabled={isPendingUpdateAppConfig} > <Trash2 className="w-4 h-4" /> </Button> )} </td> </tr> ); }; export function AgentProfiles({ agents = [], getIsAgentDisabledForProfile, isPendingUpdateAppConfig, mcpServers, profiles, setAgentsList, setProfiles, setToolGroups, toolGroups, }: { agents: string[]; getIsAgentDisabledForProfile: ({ profileId, agentId, }: { profileId: string; agentId: string; }) => boolean; isPendingUpdateAppConfig: boolean; mcpServers: { name: string; tools: { name: string; description: string | undefined; }[]; }[]; profiles: AgentProfile[]; setAgentsList: (updater: (agents: string[]) => string[]) => void; setProfiles: (updater: (profiles: AgentProfile[]) => AgentProfile[]) => void; setToolGroups: (updater: (toolGroups: ToolGroup[]) => ToolGroup[]) => void; toolGroups: ToolGroup[]; }) { const [showCreateToolGroup, setShowCreateToolGroup] = useState(false); const [newToolGroupProfileId, setNewToolGroupProfileId] = useState(""); const [showInfoSection, setShowInfoSection] = useState(false); const handleProfileChange = ( id: string, field: keyof AgentProfile, value: AgentProfile[keyof AgentProfile], ) => { setProfiles((prev) => prev.map((p) => (p.id === id ? { ...p, [field]: value } : p)), ); }; const addProfile = () => { let profileNumber = 1; let profileName = `Profile ${profileNumber}`; // Unique profile name while (profiles.some((p) => p.name === profileName)) { profileNumber++; profileName = `Profile ${profileNumber}`; } const newProfile: AgentProfile = { id: `profile_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, name: profileName, permission: "allow", agents: [], toolGroups: [], }; setProfiles((prev) => [...prev, newProfile]); }; const duplicateProfile = (profileId: string) => { const profile = profiles.find((p) => p.id === profileId); if (!profile) return; let copyNumber = 1; let copyName = `${profile.name} (Copy ${copyNumber})`; // Unique copy name while (profiles.some((p) => p.name === copyName)) { copyNumber++; copyName = `${profile.name} (Copy ${copyNumber})`; } const newProfile: AgentProfile = { ...profile, id: `profile_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, name: copyName, agents: [], }; setProfiles((prev) => [...prev, newProfile]); }; const removeProfile = (profileId: string) => { setProfiles((prev) => prev.filter((p) => p.id !== profileId)); }; const saveNewToolGroup = (newGroup: ToolGroup) => { newGroup = { ...newGroup, id: `tool_group_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, }; setToolGroups((prev) => [...prev, newGroup]); handleProfileChange(newToolGroupProfileId, "toolGroups", [ ...(profiles.find((p) => p.id === newToolGroupProfileId)?.toolGroups || []), newGroup.id, ]); setNewToolGroupProfileId(""); setShowCreateToolGroup(false); }; const handleCreateNewAgent = (newAgentName: string) => { setAgentsList((prev) => [...new Set([...prev, newAgentName])]); }; const handleCreateNewToolGroup = (id: string) => { setNewToolGroupProfileId(id); setShowCreateToolGroup(true); }; const allAgentsOptions = agents.map((agent) => ({ label: agent, value: agent, disabled: false, })); return ( <div className="space-y-6"> <div className="flex justify-between items-center"> <div className="flex items-center gap-2"> <h3 className="text-lg font-medium flex items-center cursor-pointer hover:text-[var(--color-fg-interactive)]" onClick={() => setShowInfoSection(!showInfoSection)} > Agent Profile Permissions <Info className="ml-2 w-4 h-4 text-[var(--color-fg-interactive)] hover:text-[var(--color-fg-interactive-hover)] cursor-pointer" onClick={(e) => { e.stopPropagation(); setShowInfoSection(!showInfoSection); }} /> </h3> </div> <Button onClick={addProfile} size="sm" variant="secondary" className="px-2 border-[var(--color-border-interactive)] text-[var(--color-fg-interactive)] hover:bg-[var(--color-bg-interactive-hover)] hover:text-[var(--color-fg-interactive-hover)] focus:text-[var(--color-fg-interactive-hover)] focus:bg-[var(--color-bg-interactive-hover)]" > <Plus className="w-4 h-4 mr-2" /> Add Profile </Button> </div> {showInfoSection && ( <div className="text-sm text-[var(--color-text-secondary)] bg-[var(--color-bg-neutral)] p-3 rounded-lg border border-[var(--color-border-primary)]"> <p> Each agent can be assigned to only one profile, unassigned agents will use the "Default" profile. </p> <p className="mt-2 text-xs"> Available agents:{" "} { agents.filter( (agent) => !profiles.some( (p) => p.name !== DEFAULT_PROFILE_NAME && p.agents.includes(agent), ), ).length }{" "} of {agents.length} </p> </div> )} {showCreateToolGroup && ( <ToolGroupModal mcpServers={mcpServers} onClose={() => setShowCreateToolGroup(false)} saveToolGroup={saveNewToolGroup} toolGroups={toolGroups} /> )} <div className="border border-[var(--color-border-primary)] rounded-lg overflow-x-auto"> <table className="w-full text-sm"> <thead className="bg-[var(--color-bg-neutral)]"> <tr className="text-left font-medium"> <th className="p-3">Profile</th> <th className="p-3">Agents</th> <th className="p-3">Permission Type</th> <th className="p-3">Tools</th> <th className="p-3 text-end">Actions</th> </tr> </thead> <tbody> {profiles.map((profile, index) => ( <AgentProfileRow key={profile.id} agentsOptions={allAgentsOptions.map((agent) => ({ disabled: getIsAgentDisabledForProfile({ profileId: profile.id, agentId: agent.value, }), label: agent.label, value: agent.value, }))} handleProfileChange={handleProfileChange} isDefaultProfile={index === 0} isPendingUpdateAppConfig={isPendingUpdateAppConfig} onCreateNewAgent={handleCreateNewAgent} onCreateNewToolGroup={() => handleCreateNewToolGroup(profile.id) } onDuplicateProfile={duplicateProfile} onRemoveProfile={removeProfile} profile={profile} toolGroups={toolGroups} /> ))} </tbody> </table> </div> </div> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TheLunarCompany/lunar'

If you have feedback or need assistance with the MCP directory API, please join our Discord server