{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "agent-config-studio", "title": "Agent Config Studio", "description": "Responsive controlled master/detail configuration tool for Provider, Endpoint, Model, Service, MCP, Persona, and Skill resources", "dependencies": ["lucide-react"], "registryDependencies": [ "https://ui-components.wener.me/r/ai-provider-editor.json", "https://ui-components.wener.me/r/ai-endpoint-editor.json", "https://ui-components.wener.me/r/ai-model-editor.json", "https://ui-components.wener.me/r/ai-service-editor.json", "https://ui-components.wener.me/r/mcp-server-editor.json", "https://ui-components.wener.me/r/agent-persona-editor.json", "https://ui-components.wener.me/r/agent-skill-editor.json" ], "files": [ { "path": "registry/default/blocks/agent-config-studio/agent-config-studio-types.ts", "content": "import type { Persona } from '@wener/ai/agent/persona';\nimport type { Skill } from '@wener/ai/agent/skill';\nimport type { McpServerConfig } from '@wener/ai/mcp';\nimport type { Endpoint, Model, Provider, Service } from '@wener/ai/schema';\nimport type { ComponentPropsWithRef, ReactNode } from 'react';\n\nexport type AgentConfigResourceKind = 'provider' | 'endpoint' | 'model' | 'service' | 'mcp' | 'persona' | 'skill';\nexport type AgentConfigStudioSelection = { kind: AgentConfigResourceKind; id?: string };\nexport type AgentConfigStudioMessages = {\n\ttitle: string;\n\tdescription: string;\n\tresourceList: string;\n\tresourceTypes: string;\n\temptyList: string;\n\temptySelection: string;\n\tcreate: string;\n\tremove: string;\n\tidentityConflict: (id: string) => string;\n\tkinds: Record;\n};\nexport type AgentConfigStudioProps = Omit, 'onChange' | 'title'> & {\n\tproviders: readonly Provider[];\n\tendpoints: readonly Endpoint[];\n\tmodels: readonly Model[];\n\tservices: readonly Service[];\n\tmcpServers: readonly McpServerConfig[];\n\tpersonas: readonly Persona[];\n\tskills: readonly Skill[];\n\tonProvidersChange: (value: Provider[]) => void;\n\tonEndpointsChange: (value: Endpoint[]) => void;\n\tonModelsChange: (value: Model[]) => void;\n\tonServicesChange: (value: Service[]) => void;\n\tonMcpServersChange: (value: McpServerConfig[]) => void;\n\tonPersonasChange: (value: Persona[]) => void;\n\tonSkillsChange: (value: Skill[]) => void;\n\tselection: AgentConfigStudioSelection;\n\tonSelectionChange: (value: AgentConfigStudioSelection) => void;\n\tonCreate?: (kind: AgentConfigResourceKind) => void;\n\tonRemove?: (selection: AgentConfigStudioSelection) => void;\n\tdisabled?: boolean;\n\treadOnly?: boolean;\n\tmessages?: Partial> & {\n\t\tkinds?: Partial;\n\t};\n\tempty?: ReactNode;\n};\n", "type": "registry:lib", "target": "@components/blocks/agent-config-studio/agent-config-studio-types.ts" }, { "path": "registry/default/blocks/agent-config-studio/agent-config-studio-editor.tsx", "content": "'use client';\n\nimport { type Persona, PersonaSchema } from '@wener/ai/agent/persona';\nimport { getSkillIdentity, type Skill, SkillSchema } from '@wener/ai/agent/skill';\nimport { type McpServerConfig, McpServerConfigSchema } from '@wener/ai/mcp';\nimport {\n\ttype Endpoint,\n\tEndpointSchema,\n\ttype Model,\n\tModelSchema,\n\ttype Provider,\n\tProviderSchema,\n\ttype Service,\n\tServiceSchema,\n} from '@wener/ai/schema';\nimport type { ReactNode } from 'react';\nimport type { AiConfigSchema } from '../../ui/ai-config-editor';\nimport { AgentPersonaEditor } from '../agent-persona-editor';\nimport { AgentSkillEditor } from '../agent-skill-editor';\nimport { AiEndpointEditor } from '../ai-endpoint-editor';\nimport { AiModelEditor } from '../ai-model-editor';\nimport { AiProviderEditor } from '../ai-provider-editor';\nimport { AiServiceEditor } from '../ai-service-editor';\nimport { McpServerEditor } from '../mcp-server-editor';\nimport type {\n\tAgentConfigResourceKind,\n\tAgentConfigStudioMessages,\n\tAgentConfigStudioProps,\n\tAgentConfigStudioSelection,\n} from './agent-config-studio-types';\n\nexport type ResourceCollections = Pick<\n\tAgentConfigStudioProps,\n\t'providers' | 'endpoints' | 'models' | 'services' | 'mcpServers' | 'personas' | 'skills'\n>;\ntype StudioEditorContext = ResourceCollections &\n\tPick<\n\t\tAgentConfigStudioProps,\n\t\t| 'onProvidersChange'\n\t\t| 'onEndpointsChange'\n\t\t| 'onModelsChange'\n\t\t| 'onServicesChange'\n\t\t| 'onMcpServersChange'\n\t\t| 'onPersonasChange'\n\t\t| 'onSkillsChange'\n\t\t| 'onSelectionChange'\n\t\t| 'disabled'\n\t\t| 'readOnly'\n\t> & { selection: AgentConfigStudioSelection };\nexport type StudioResourceItem = { id: string; label: string };\n\nexport function resourcesForKind(kind: AgentConfigResourceKind, values: ResourceCollections): StudioResourceItem[] {\n\tswitch (kind) {\n\t\tcase 'provider':\n\t\t\treturn values.providers.map((item) => ({ id: item.id, label: item.title || item.name }));\n\t\tcase 'endpoint':\n\t\t\treturn values.endpoints.map((item) => ({ id: item.id, label: item.title || item.name }));\n\t\tcase 'model':\n\t\t\treturn values.models.map((item) => ({ id: item.id, label: item.title || item.name }));\n\t\tcase 'service':\n\t\t\treturn values.services.map((item) => ({ id: item.id, label: item.title || item.name }));\n\t\tcase 'mcp':\n\t\t\treturn values.mcpServers.map((item, index) => ({\n\t\t\t\tid: mcpIdentity(item, index),\n\t\t\t\tlabel: item.title || item.name || item.id || `MCP Server ${index + 1}`,\n\t\t\t}));\n\t\tcase 'persona':\n\t\t\treturn values.personas.map((item) => ({ id: item.id, label: item.title || item.name }));\n\t\tcase 'skill':\n\t\t\treturn values.skills.map((item) => ({ id: skillIdentity(item), label: item.name }));\n\t}\n}\n\nexport function renderSelectedStudioEditor(\n\tcontext: StudioEditorContext,\n\tmessages: AgentConfigStudioMessages,\n): ReactNode {\n\tconst { selection, disabled, readOnly } = context;\n\tconst providerOptions = context.providers.map((entry) => ({ value: entry.id, label: entry.title || entry.name }));\n\tswitch (selection.kind) {\n\t\tcase 'provider': {\n\t\t\tconst item = context.providers.find((entry) => entry.id === selection.id);\n\t\t\treturn item ? (\n\t\t\t\t\n\t\t\t\t\t\tupdateStudioResource(\n\t\t\t\t\t\t\tcontext.providers,\n\t\t\t\t\t\t\titem,\n\t\t\t\t\t\t\tnext,\n\t\t\t\t\t\t\t'provider',\n\t\t\t\t\t\t\tnext.id,\n\t\t\t\t\t\t\tcontext.onProvidersChange,\n\t\t\t\t\t\t\tcontext.onSelectionChange,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t) : null;\n\t\t}\n\t\tcase 'endpoint': {\n\t\t\tconst item = context.endpoints.find((entry) => entry.id === selection.id);\n\t\t\treturn item ? (\n\t\t\t\t\n\t\t\t\t\t\tupdateStudioResource(\n\t\t\t\t\t\t\tcontext.endpoints,\n\t\t\t\t\t\t\titem,\n\t\t\t\t\t\t\tnext,\n\t\t\t\t\t\t\t'endpoint',\n\t\t\t\t\t\t\tnext.id,\n\t\t\t\t\t\t\tcontext.onEndpointsChange,\n\t\t\t\t\t\t\tcontext.onSelectionChange,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t) : null;\n\t\t}\n\t\tcase 'model': {\n\t\t\tconst item = context.models.find((entry) => entry.id === selection.id);\n\t\t\treturn item ? (\n\t\t\t\t\n\t\t\t\t\t\tupdateStudioResource(\n\t\t\t\t\t\t\tcontext.models,\n\t\t\t\t\t\t\titem,\n\t\t\t\t\t\t\tnext,\n\t\t\t\t\t\t\t'model',\n\t\t\t\t\t\t\tnext.id,\n\t\t\t\t\t\t\tcontext.onModelsChange,\n\t\t\t\t\t\t\tcontext.onSelectionChange,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t) : null;\n\t\t}\n\t\tcase 'service': {\n\t\t\tconst item = context.services.find((entry) => entry.id === selection.id);\n\t\t\treturn item ? (\n\t\t\t\t\n\t\t\t\t\t\tupdateStudioResource(\n\t\t\t\t\t\t\tcontext.services,\n\t\t\t\t\t\t\titem,\n\t\t\t\t\t\t\tnext,\n\t\t\t\t\t\t\t'service',\n\t\t\t\t\t\t\tnext.id,\n\t\t\t\t\t\t\tcontext.onServicesChange,\n\t\t\t\t\t\t\tcontext.onSelectionChange,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t) : null;\n\t\t}\n\t\tcase 'mcp': {\n\t\t\tconst index = context.mcpServers.findIndex((entry, itemIndex) => mcpIdentity(entry, itemIndex) === selection.id);\n\t\t\tconst item = context.mcpServers[index];\n\t\t\treturn item ? (\n\t\t\t\t mcpIdentity(entry, context.mcpServers.indexOf(entry)),\n\t\t\t\t\t\t'id',\n\t\t\t\t\t\tmessages,\n\t\t\t\t\t)}\n\t\t\t\t\tdisabled={disabled}\n\t\t\t\t\treadOnly={readOnly}\n\t\t\t\t\tonChange={(next) =>\n\t\t\t\t\t\tupdateStudioResource(\n\t\t\t\t\t\t\tcontext.mcpServers,\n\t\t\t\t\t\t\titem,\n\t\t\t\t\t\t\tnext,\n\t\t\t\t\t\t\t'mcp',\n\t\t\t\t\t\t\tnext.id || next.name || `mcp-${index + 1}`,\n\t\t\t\t\t\t\tcontext.onMcpServersChange,\n\t\t\t\t\t\t\tcontext.onSelectionChange,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t) : null;\n\t\t}\n\t\tcase 'persona': {\n\t\t\tconst item = context.personas.find((entry) => entry.id === selection.id);\n\t\t\treturn item ? (\n\t\t\t\t\n\t\t\t\t\t\tupdateStudioResource(\n\t\t\t\t\t\t\tcontext.personas,\n\t\t\t\t\t\t\titem,\n\t\t\t\t\t\t\tnext,\n\t\t\t\t\t\t\t'persona',\n\t\t\t\t\t\t\tnext.id,\n\t\t\t\t\t\t\tcontext.onPersonasChange,\n\t\t\t\t\t\t\tcontext.onSelectionChange,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t) : null;\n\t\t}\n\t\tcase 'skill': {\n\t\t\tconst item = context.skills.find((entry) => skillIdentity(entry) === selection.id);\n\t\t\treturn item ? (\n\t\t\t\t\n\t\t\t\t\t\tupdateStudioResource(\n\t\t\t\t\t\t\tcontext.skills,\n\t\t\t\t\t\t\titem,\n\t\t\t\t\t\t\tnext,\n\t\t\t\t\t\t\t'skill',\n\t\t\t\t\t\t\tskillIdentity(next),\n\t\t\t\t\t\t\tcontext.onSkillsChange,\n\t\t\t\t\t\t\tcontext.onSelectionChange,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t) : null;\n\t\t}\n\t}\n}\n\nfunction uniqueIdentitySchema(\n\tbase: AiConfigSchema,\n\titems: readonly T[],\n\tcurrent: T,\n\tidentity: (value: T) => string,\n\tfield: string,\n\tmessages: AgentConfigStudioMessages,\n): AiConfigSchema {\n\treturn {\n\t\tsafeParse(value) {\n\t\t\tconst parsed = base.safeParse(value);\n\t\t\tif (!parsed.success) return parsed;\n\t\t\tconst nextId = identity(parsed.data);\n\t\t\tif (items.some((item) => item !== current && identity(item) === nextId)) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: { issues: [{ path: [field], message: messages.identityConflict(nextId) }] },\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn parsed;\n\t\t},\n\t};\n}\n\nfunction updateStudioResource(\n\titems: readonly T[],\n\tcurrent: T,\n\tnext: T,\n\tkind: AgentConfigResourceKind,\n\tnextId: string,\n\tonChange: (items: T[]) => void,\n\tonSelectionChange: (selection: AgentConfigStudioSelection) => void,\n): void {\n\tonChange(items.map((item) => (item === current ? next : item)));\n\tonSelectionChange({ kind, id: nextId });\n}\n\nexport function endpointOptionsForProvider(endpoints: readonly Endpoint[], providerId?: string) {\n\treturn endpoints\n\t\t.filter((endpoint) => !providerId || !endpoint.providerId || endpoint.providerId === providerId)\n\t\t.map((endpoint) => ({\n\t\t\tvalue: endpoint.id,\n\t\t\tlabel: endpointOptionLabel(endpoint),\n\t\t}));\n}\n\nfunction endpointOptionLabel(endpoint: Endpoint): string {\n\tconst label = endpoint.title || endpoint.name;\n\treturn label === endpoint.id ? endpoint.id : `${label} · ${endpoint.id}`;\n}\n\nfunction providerIdentity(value: Provider): string {\n\treturn value.id;\n}\nfunction endpointIdentity(value: Endpoint): string {\n\treturn value.id;\n}\nfunction modelIdentity(value: Model): string {\n\treturn value.id;\n}\nfunction serviceIdentity(value: Service): string {\n\treturn value.id;\n}\nfunction personaIdentity(value: Persona): string {\n\treturn value.id;\n}\nfunction skillIdentity(value: Skill): string {\n\treturn getSkillIdentity(value);\n}\nfunction mcpIdentity(value: McpServerConfig, index: number): string {\n\treturn value.id || value.name || `mcp-${index + 1}`;\n}\n", "type": "registry:component", "target": "@components/blocks/agent-config-studio/agent-config-studio-editor.tsx" }, { "path": "registry/default/blocks/agent-config-studio/agent-config-studio.tsx", "content": "'use client';\n\nimport { Bot, Box, BrainCircuit, Cable, Network, Plus, ServerCog, Sparkles, Trash2 } from 'lucide-react';\nimport type { ComponentType, KeyboardEvent } from 'react';\nimport { useId, useRef } from 'react';\nimport { type ResourceCollections, renderSelectedStudioEditor, resourcesForKind } from './agent-config-studio-editor';\nimport type {\n\tAgentConfigResourceKind,\n\tAgentConfigStudioMessages,\n\tAgentConfigStudioProps,\n} from './agent-config-studio-types';\n\nconst defaultMessages: AgentConfigStudioMessages = {\n\ttitle: 'Agent 配置工作室',\n\tdescription: '集中浏览独立资源,并使用 canonical editor 修改当前选择。',\n\tresourceList: '资源列表',\n\tresourceTypes: '配置资源类型',\n\temptyList: '此分类暂无资源',\n\temptySelection: '请选择一个资源开始编辑',\n\tcreate: '新建资源',\n\tremove: '删除资源',\n\tidentityConflict: (id) => `资源 ID“${id}”已存在,请使用不同的 ID。`,\n\tkinds: {\n\t\tprovider: 'AI 提供方',\n\t\tendpoint: 'AI Endpoint',\n\t\tmodel: 'AI 模型',\n\t\tservice: 'AI 服务',\n\t\tmcp: 'MCP 服务',\n\t\tpersona: '角色',\n\t\tskill: '技能',\n\t},\n};\n\nconst kinds: readonly {\n\tkind: AgentConfigResourceKind;\n\ticon: ComponentType<{ className?: string; 'aria-hidden'?: 'true' }>;\n}[] = [\n\t{ kind: 'provider', icon: Cable },\n\t{ kind: 'endpoint', icon: Network },\n\t{ kind: 'model', icon: BrainCircuit },\n\t{ kind: 'service', icon: ServerCog },\n\t{ kind: 'mcp', icon: Box },\n\t{ kind: 'persona', icon: Bot },\n\t{ kind: 'skill', icon: Sparkles },\n];\n\nexport function AgentConfigStudio({\n\tproviders,\n\tendpoints,\n\tmodels,\n\tservices,\n\tmcpServers,\n\tpersonas,\n\tskills,\n\tonProvidersChange,\n\tonEndpointsChange,\n\tonModelsChange,\n\tonServicesChange,\n\tonMcpServersChange,\n\tonPersonasChange,\n\tonSkillsChange,\n\tselection,\n\tonSelectionChange,\n\tonCreate,\n\tonRemove,\n\tdisabled = false,\n\treadOnly = false,\n\tmessages: overrides,\n\tempty,\n\tclassName,\n\t...props\n}: AgentConfigStudioProps) {\n\tconst messages: AgentConfigStudioMessages = {\n\t\t...defaultMessages,\n\t\t...overrides,\n\t\tkinds: { ...defaultMessages.kinds, ...overrides?.kinds },\n\t};\n\tconst id = useId();\n\tconst tabRefs = useRef>>({});\n\tconst collections = { providers, endpoints, models, services, mcpServers, personas, skills };\n\tconst counts = {\n\t\tprovider: providers.length,\n\t\tendpoint: endpoints.length,\n\t\tmodel: models.length,\n\t\tservice: services.length,\n\t\tmcp: mcpServers.length,\n\t\tpersona: personas.length,\n\t\tskill: skills.length,\n\t};\n\tconst selectKind = (kind: AgentConfigResourceKind) => {\n\t\tconst first = resourcesForKind(kind, collections)[0];\n\t\tonSelectionChange({ kind, id: first?.id });\n\t};\n\tconst moveTabFocus = (event: KeyboardEvent, currentIndex: number) => {\n\t\tlet nextIndex: number | undefined;\n\t\tif (event.key === 'ArrowRight') nextIndex = (currentIndex + 1) % kinds.length;\n\t\telse if (event.key === 'ArrowLeft') nextIndex = (currentIndex - 1 + kinds.length) % kinds.length;\n\t\telse if (event.key === 'Home') nextIndex = 0;\n\t\telse if (event.key === 'End') nextIndex = kinds.length - 1;\n\t\tif (nextIndex === undefined) return;\n\t\tevent.preventDefault();\n\t\tconst nextKind = kinds[nextIndex]!.kind;\n\t\tselectKind(nextKind);\n\t\ttabRefs.current[nextKind]?.focus();\n\t};\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t

{messages.title}

\n\t\t\t\t

{messages.description}

\n\t\t\t
\n\t\t\t\n\t\t\t\t{kinds.map(({ kind, icon: Icon }, index) => (\n\t\t\t\t\t {\n\t\t\t\t\t\t\ttabRefs.current[kind] = element;\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tkey={kind}\n\t\t\t\t\t\tid={`${id}-tab-${kind}`}\n\t\t\t\t\t\ttype='button'\n\t\t\t\t\t\trole='tab'\n\t\t\t\t\t\taria-controls={`${id}-panel-${kind}`}\n\t\t\t\t\t\taria-selected={selection.kind === kind}\n\t\t\t\t\t\ttabIndex={selection.kind === kind ? 0 : -1}\n\t\t\t\t\t\tclassName={`btn btn-sm shrink-0 ${selection.kind === kind ? 'btn-active' : 'btn-ghost'}`}\n\t\t\t\t\t\tonClick={() => selectKind(kind)}\n\t\t\t\t\t\tonKeyDown={(event) => moveTabFocus(event, index)}\n\t\t\t\t\t>\n\t\t\t\t\t\t
\n\t);\n}\n\ntype StudioKindPanelProps = {\n\tkind: AgentConfigResourceKind;\n\tcollections: ResourceCollections;\n\tmessages: AgentConfigStudioMessages;\n\tselectionId?: string;\n\tdisabled: boolean;\n\treadOnly: boolean;\n\tonCreate?: AgentConfigStudioProps['onCreate'];\n\tonRemove?: AgentConfigStudioProps['onRemove'];\n\tempty?: AgentConfigStudioProps['empty'];\n\teditorProps: Pick<\n\t\tAgentConfigStudioProps,\n\t\t| 'onProvidersChange'\n\t\t| 'onEndpointsChange'\n\t\t| 'onModelsChange'\n\t\t| 'onServicesChange'\n\t\t| 'onMcpServersChange'\n\t\t| 'onPersonasChange'\n\t\t| 'onSkillsChange'\n\t\t| 'onSelectionChange'\n\t>;\n};\n\nfunction StudioKindPanel({\n\tkind,\n\tcollections,\n\tmessages,\n\tselectionId,\n\tdisabled,\n\treadOnly,\n\tonCreate,\n\tonRemove,\n\tempty,\n\teditorProps,\n}: StudioKindPanelProps) {\n\tconst resources = resourcesForKind(kind, collections);\n\tconst selected = resources.find((resource) => resource.id === selectionId);\n\tconst selection = { kind, id: selectionId };\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t
\n\t\t\t\t{selected ? (\n\t\t\t\t\t
\n\t\t\t\t\t\t{onRemove && !readOnly ? (\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t onRemove(selection)}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t{renderSelectedStudioEditor({ ...collections, ...editorProps, selection, disabled, readOnly }, messages)}\n\t\t\t\t\t
\n\t\t\t\t) : (\n\t\t\t\t\t(empty ?? (\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t))\n\t\t\t\t)}\n\t\t\t
\n\t\t
\n\t);\n}\n", "type": "registry:component", "target": "@components/blocks/agent-config-studio/agent-config-studio.tsx" }, { "path": "registry/default/blocks/agent-config-studio/index.ts", "content": "export * from './agent-config-studio';\nexport * from './agent-config-studio-types';\n", "type": "registry:component", "target": "@components/blocks/agent-config-studio/index.ts" } ], "type": "registry:block" }