diff --git a/apps/backend/internal/agent/discovery/agents.json b/apps/backend/internal/agent/discovery/agents.json index ed3f37525f..f53d2d3c00 100644 --- a/apps/backend/internal/agent/discovery/agents.json +++ b/apps/backend/internal/agent/discovery/agents.json @@ -1,35 +1,8 @@ { "agents": [ - { - "name": "claude", - "supports_mcp": false, - "mcp_config_path": { - "linux": [], - "windows": [], - "macos": [] - }, - "installation_path": { - "linux": ["~/.claude.json"], - "windows": ["~/.claude.json"], - "macos": ["~/.claude.json"] - } - }, - { - "name": "gemini", - "supports_mcp": false, - "mcp_config_path": { - "linux": [], - "windows": [], - "macos": [] - }, - "installation_path": { - "linux": ["~/.gemini/oauth_creds.json"], - "windows": ["~/.gemini/oauth_creds.json"], - "macos": ["~/.gemini/oauth_creds.json"] - } - }, { "name": "codex", + "display_name": "Codex", "supports_mcp": false, "mcp_config_path": { "linux": [], @@ -40,10 +13,16 @@ "linux": ["~/.codex/auth.json"], "windows": ["~/.codex/auth.json"], "macos": ["~/.codex/auth.json"] + }, + "capabilities": { + "supports_session_resume": false, + "supports_shell": false, + "supports_workspace_only": false } }, { "name": "auggie", + "display_name": "Auggie", "supports_mcp": false, "mcp_config_path": { "linux": [], @@ -54,45 +33,11 @@ "linux": ["~/.augment/.auggie.json"], "windows": ["~/.augment/.auggie.json"], "macos": ["~/.augment/.auggie.json"] - } - }, - { - "name": "opencode", - "supports_mcp": false, - "mcp_config_path": { - "linux": [], - "windows": [], - "macos": [] }, - "installation_path": { - "linux": [ - "$XDG_CONFIG_HOME/opencode/opencode.json", - "~/.config/opencode/opencode.json", - "$XDG_CONFIG_HOME/opencode", - "~/.config/opencode" - ], - "windows": [ - "~/AppData/Roaming/opencode/opencode.json", - "~/AppData/Roaming/opencode" - ], - "macos": [ - "~/Library/Application Support/opencode/opencode.json", - "~/Library/Application Support/opencode" - ] - } - }, - { - "name": "copilot", - "supports_mcp": true, - "mcp_config_path": { - "linux": ["~/.copilot/mcp-config.json"], - "windows": ["~/.copilot/mcp-config.json"], - "macos": ["~/.copilot/mcp-config.json"] - }, - "installation_path": { - "linux": ["~/.copilot/mcp-config.json", "~/.copilot/config.json"], - "windows": ["~/.copilot/mcp-config.json", "~/.copilot/config.json"], - "macos": ["~/.copilot/mcp-config.json", "~/.copilot/config.json"] + "capabilities": { + "supports_session_resume": false, + "supports_shell": false, + "supports_workspace_only": false } } ] diff --git a/apps/backend/internal/agent/discovery/discovery.go b/apps/backend/internal/agent/discovery/discovery.go index ed8e4b71d9..18dbfc7c65 100644 --- a/apps/backend/internal/agent/discovery/discovery.go +++ b/apps/backend/internal/agent/discovery/discovery.go @@ -20,6 +20,12 @@ type OSPaths struct { MacOS []string `json:"macos"` } +type Capabilities struct { + SupportsSessionResume bool `json:"supports_session_resume"` + SupportsShell bool `json:"supports_shell"` + SupportsWorkspaceOnly bool `json:"supports_workspace_only"` +} + func (p OSPaths) ForOS(goos string) []string { switch goos { case "windows": @@ -32,10 +38,12 @@ func (p OSPaths) ForOS(goos string) []string { } type KnownAgent struct { - Name string `json:"name"` - SupportsMCP bool `json:"supports_mcp"` - MCPConfigPath OSPaths `json:"mcp_config_path"` - InstallationPath OSPaths `json:"installation_path"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + SupportsMCP bool `json:"supports_mcp"` + MCPConfigPath OSPaths `json:"mcp_config_path"` + InstallationPath OSPaths `json:"installation_path"` + Capabilities Capabilities `json:"capabilities"` } type Config struct { @@ -56,7 +64,8 @@ type Adapter interface { } type Registry struct { - adapters []Adapter + adapters []Adapter + definitions []KnownAgent } func LoadRegistry() (*Registry, error) { @@ -72,7 +81,14 @@ func LoadRegistry() (*Registry, error) { for _, agent := range cfg.Agents { adapters = append(adapters, NewFilePresenceAdapter(agent)) } - return &Registry{adapters: adapters}, nil + return &Registry{adapters: adapters, definitions: cfg.Agents}, nil +} + +func (r *Registry) Definitions() []KnownAgent { + if r == nil { + return nil + } + return append([]KnownAgent(nil), r.definitions...) } func (r *Registry) Detect(ctx context.Context) ([]Availability, error) { diff --git a/apps/backend/internal/agent/settings/controller/controller.go b/apps/backend/internal/agent/settings/controller/controller.go index a37ce712f1..6cc2858f8c 100644 --- a/apps/backend/internal/agent/settings/controller/controller.go +++ b/apps/backend/internal/agent/settings/controller/controller.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "strings" + "time" "github.com/kandev/kandev/internal/agent/discovery" "github.com/kandev/kandev/internal/agent/settings/dto" @@ -62,6 +63,51 @@ func (c *Controller) ListDiscovery(ctx context.Context) (*dto.ListDiscoveryRespo return &dto.ListDiscoveryResponse{Agents: payload, Total: len(payload)}, nil } +func (c *Controller) ListAvailableAgents(ctx context.Context) (*dto.ListAvailableAgentsResponse, error) { + results, err := c.discovery.Detect(ctx) + if err != nil { + return nil, err + } + availabilityByName := make(map[string]discovery.Availability, len(results)) + for _, result := range results { + availabilityByName[result.Name] = result + } + definitions := c.discovery.Definitions() + now := time.Now().UTC() + payload := make([]dto.AvailableAgentDTO, 0, len(definitions)) + for _, def := range definitions { + availability, ok := availabilityByName[def.Name] + if !ok { + availability = discovery.Availability{ + Name: def.Name, + SupportsMCP: def.SupportsMCP, + MCPConfigPath: "", + Available: false, + } + } + displayName := def.DisplayName + if displayName == "" { + displayName = def.Name + } + payload = append(payload, dto.AvailableAgentDTO{ + Name: availability.Name, + DisplayName: displayName, + SupportsMCP: availability.SupportsMCP, + MCPConfigPath: availability.MCPConfigPath, + InstallationPaths: availability.InstallationPaths, + Available: availability.Available, + MatchedPath: availability.MatchedPath, + Capabilities: dto.AgentCapabilitiesDTO{ + SupportsSessionResume: def.Capabilities.SupportsSessionResume, + SupportsShell: def.Capabilities.SupportsShell, + SupportsWorkspaceOnly: def.Capabilities.SupportsWorkspaceOnly, + }, + UpdatedAt: now, + }) + } + return &dto.ListAvailableAgentsResponse{Agents: payload, Total: len(payload)}, nil +} + func (c *Controller) ListAgents(ctx context.Context) (*dto.ListAgentsResponse, error) { agents, err := c.repo.ListAgents(ctx) if err != nil { @@ -83,10 +129,15 @@ func (c *Controller) EnsureInitialAgentProfiles(ctx context.Context) error { if err != nil { return err } + displayNameByAgent := c.displayNameByAgent() for _, result := range results { if !result.Available { continue } + displayName, ok := displayNameByAgent[result.Name] + if !ok || displayName == "" { + return fmt.Errorf("unknown agent display name: %s", result.Name) + } agent, err := c.repo.GetAgentByName(ctx, result.Name) if err != nil && !errors.Is(err, sql.ErrNoRows) { return err @@ -109,10 +160,11 @@ func (c *Controller) EnsureInitialAgentProfiles(ctx context.Context) error { continue } defaultProfile := &models.AgentProfile{ - AgentID: agent.ID, - Name: defaultAgentProfileName, - Model: "", - Plan: "", + AgentID: agent.ID, + Name: defaultAgentProfileName, + Model: "", + Plan: "", + AgentDisplayName: displayName, } if err := c.repo.CreateAgentProfile(ctx, defaultProfile); err != nil { return err @@ -179,6 +231,10 @@ func (c *Controller) CreateAgent(ctx context.Context, req CreateAgentRequest) (* if !matched.Available { return nil, fmt.Errorf("agent not installed: %s", req.Name) } + displayName := c.mustDisplayName(req.Name) + if displayName == "" { + return nil, fmt.Errorf("unknown agent display name: %s", req.Name) + } agent := &models.Agent{ Name: matched.Name, WorkspaceID: req.WorkspaceID, @@ -193,6 +249,7 @@ func (c *Controller) CreateAgent(ctx context.Context, req CreateAgentRequest) (* profile := &models.AgentProfile{ AgentID: agent.ID, Name: profileReq.Name, + AgentDisplayName: displayName, Model: profileReq.Model, AutoApprove: profileReq.AutoApprove, DangerouslySkipPermissions: profileReq.DangerouslySkipPermissions, @@ -259,9 +316,18 @@ type CreateProfileRequest struct { } func (c *Controller) CreateProfile(ctx context.Context, req CreateProfileRequest) (*dto.AgentProfileDTO, error) { + agent, err := c.repo.GetAgent(ctx, req.AgentID) + if err != nil { + return nil, err + } + displayName := c.mustDisplayName(agent.Name) + if displayName == "" { + return nil, fmt.Errorf("unknown agent display name: %s", agent.Name) + } profile := &models.AgentProfile{ AgentID: req.AgentID, Name: req.Name, + AgentDisplayName: displayName, Model: req.Model, AutoApprove: req.AutoApprove, DangerouslySkipPermissions: req.DangerouslySkipPermissions, @@ -359,6 +425,7 @@ func toProfileDTO(profile *models.AgentProfile) dto.AgentProfileDTO { ID: profile.ID, AgentID: profile.AgentID, Name: profile.Name, + AgentDisplayName: profile.AgentDisplayName, Model: profile.Model, AutoApprove: profile.AutoApprove, DangerouslySkipPermissions: profile.DangerouslySkipPermissions, @@ -367,3 +434,28 @@ func toProfileDTO(profile *models.AgentProfile) dto.AgentProfileDTO { UpdatedAt: profile.UpdatedAt, } } + +func (c *Controller) displayNameByAgent() map[string]string { + definitions := c.discovery.Definitions() + mapped := make(map[string]string, len(definitions)) + for _, def := range definitions { + if def.Name == "" || def.DisplayName == "" { + continue + } + mapped[def.Name] = def.DisplayName + } + return mapped +} + +func (c *Controller) mustDisplayName(agentName string) string { + if agentName == "" { + return "" + } + definitions := c.discovery.Definitions() + for _, def := range definitions { + if def.Name == agentName { + return def.DisplayName + } + } + return "" +} diff --git a/apps/backend/internal/agent/settings/dto/dto.go b/apps/backend/internal/agent/settings/dto/dto.go index cd16b650e0..6a322d73b2 100644 --- a/apps/backend/internal/agent/settings/dto/dto.go +++ b/apps/backend/internal/agent/settings/dto/dto.go @@ -6,6 +6,7 @@ type AgentProfileDTO struct { ID string `json:"id"` AgentID string `json:"agent_id"` Name string `json:"name"` + AgentDisplayName string `json:"agent_display_name"` Model string `json:"model"` AutoApprove bool `json:"auto_approve"` DangerouslySkipPermissions bool `json:"dangerously_skip_permissions"` @@ -43,3 +44,26 @@ type ListDiscoveryResponse struct { Agents []AgentDiscoveryDTO `json:"agents"` Total int `json:"total"` } + +type AgentCapabilitiesDTO struct { + SupportsSessionResume bool `json:"supports_session_resume"` + SupportsShell bool `json:"supports_shell"` + SupportsWorkspaceOnly bool `json:"supports_workspace_only"` +} + +type AvailableAgentDTO struct { + Name string `json:"name"` + DisplayName string `json:"display_name"` + SupportsMCP bool `json:"supports_mcp"` + MCPConfigPath string `json:"mcp_config_path,omitempty"` + InstallationPaths []string `json:"installation_paths,omitempty"` + Available bool `json:"available"` + MatchedPath string `json:"matched_path,omitempty"` + Capabilities AgentCapabilitiesDTO `json:"capabilities"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListAvailableAgentsResponse struct { + Agents []AvailableAgentDTO `json:"agents"` + Total int `json:"total"` +} diff --git a/apps/backend/internal/agent/settings/handlers/handlers.go b/apps/backend/internal/agent/settings/handlers/handlers.go index 9235131146..cd0b83fdeb 100644 --- a/apps/backend/internal/agent/settings/handlers/handlers.go +++ b/apps/backend/internal/agent/settings/handlers/handlers.go @@ -37,6 +37,7 @@ func RegisterRoutes(router *gin.Engine, ctrl *controller.Controller, hub Broadca func (h *Handlers) registerHTTP(router *gin.Engine) { api := router.Group("/api/v1") api.GET("/agents/discovery", h.httpDiscoverAgents) + api.GET("/agents/available", h.httpListAvailableAgents) api.GET("/agents", h.httpListAgents) api.POST("/agents", h.httpCreateAgent) api.GET("/agents/:id", h.httpGetAgent) @@ -54,6 +55,27 @@ func (h *Handlers) httpDiscoverAgents(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to discover agents"}) return } + if h.hub != nil { + availableResp, err := h.controller.ListAvailableAgents(c.Request.Context()) + if err != nil { + h.logger.Error("failed to list available agents", zap.Error(err)) + } else { + notification, _ := ws.NewNotification(ws.ActionAgentAvailableUpdated, gin.H{ + "agents": availableResp.Agents, + }) + h.hub.Broadcast(notification) + } + } + c.JSON(http.StatusOK, resp) +} + +func (h *Handlers) httpListAvailableAgents(c *gin.Context) { + resp, err := h.controller.ListAvailableAgents(c.Request.Context()) + if err != nil { + h.logger.Error("failed to list available agents", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list available agents"}) + return + } c.JSON(http.StatusOK, resp) } diff --git a/apps/backend/internal/agent/settings/models/models.go b/apps/backend/internal/agent/settings/models/models.go index 9dbdff6c3d..e1fcd11bc9 100644 --- a/apps/backend/internal/agent/settings/models/models.go +++ b/apps/backend/internal/agent/settings/models/models.go @@ -13,14 +13,15 @@ type Agent struct { } type AgentProfile struct { - ID string `json:"id"` - AgentID string `json:"agent_id"` - Name string `json:"name"` - Model string `json:"model"` - AutoApprove bool `json:"auto_approve"` - DangerouslySkipPermissions bool `json:"dangerously_skip_permissions"` - Plan string `json:"plan"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + AgentID string `json:"agent_id"` + Name string `json:"name"` + AgentDisplayName string `json:"agent_display_name"` + Model string `json:"model"` + AutoApprove bool `json:"auto_approve"` + DangerouslySkipPermissions bool `json:"dangerously_skip_permissions"` + Plan string `json:"plan"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` DeletedAt *time.Time `json:"deleted_at,omitempty"` } diff --git a/apps/backend/internal/agent/settings/store/sqlite.go b/apps/backend/internal/agent/settings/store/sqlite.go index 94cd03ae13..fe0222a08d 100644 --- a/apps/backend/internal/agent/settings/store/sqlite.go +++ b/apps/backend/internal/agent/settings/store/sqlite.go @@ -89,6 +89,7 @@ func (r *SQLiteRepository) initSchema() error { id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, name TEXT NOT NULL, + agent_display_name TEXT NOT NULL, model TEXT NOT NULL, auto_approve INTEGER NOT NULL DEFAULT 0, dangerously_skip_permissions INTEGER NOT NULL DEFAULT 0, @@ -110,6 +111,9 @@ func (r *SQLiteRepository) initSchema() error { if err := ensureColumn(r.db, "agent_profiles", "deleted_at", "DATETIME"); err != nil { return err } + if err := ensureColumn(r.db, "agent_profiles", "agent_display_name", "TEXT NOT NULL DEFAULT ''"); err != nil { + return err + } return nil } @@ -206,9 +210,9 @@ func (r *SQLiteRepository) CreateAgentProfile(ctx context.Context, profile *mode profile.CreatedAt = now profile.UpdatedAt = now _, err := r.db.ExecContext(ctx, ` - INSERT INTO agent_profiles (id, agent_id, name, model, auto_approve, dangerously_skip_permissions, plan, created_at, updated_at, deleted_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, profile.ID, profile.AgentID, profile.Name, profile.Model, boolToInt(profile.AutoApprove), + INSERT INTO agent_profiles (id, agent_id, name, agent_display_name, model, auto_approve, dangerously_skip_permissions, plan, created_at, updated_at, deleted_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, profile.ID, profile.AgentID, profile.Name, profile.AgentDisplayName, profile.Model, boolToInt(profile.AutoApprove), boolToInt(profile.DangerouslySkipPermissions), profile.Plan, profile.CreatedAt, profile.UpdatedAt, profile.DeletedAt) return err } @@ -217,9 +221,9 @@ func (r *SQLiteRepository) UpdateAgentProfile(ctx context.Context, profile *mode profile.UpdatedAt = time.Now().UTC() result, err := r.db.ExecContext(ctx, ` UPDATE agent_profiles - SET name = ?, model = ?, auto_approve = ?, dangerously_skip_permissions = ?, plan = ?, updated_at = ? + SET name = ?, agent_display_name = ?, model = ?, auto_approve = ?, dangerously_skip_permissions = ?, plan = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL - `, profile.Name, profile.Model, boolToInt(profile.AutoApprove), + `, profile.Name, profile.AgentDisplayName, profile.Model, boolToInt(profile.AutoApprove), boolToInt(profile.DangerouslySkipPermissions), profile.Plan, profile.UpdatedAt, profile.ID) if err != nil { return err @@ -248,7 +252,7 @@ func (r *SQLiteRepository) DeleteAgentProfile(ctx context.Context, id string) er func (r *SQLiteRepository) GetAgentProfile(ctx context.Context, id string) (*models.AgentProfile, error) { row := r.db.QueryRowContext(ctx, ` - SELECT id, agent_id, name, model, auto_approve, dangerously_skip_permissions, plan, created_at, updated_at, deleted_at + SELECT id, agent_id, name, agent_display_name, model, auto_approve, dangerously_skip_permissions, plan, created_at, updated_at, deleted_at FROM agent_profiles WHERE id = ? AND deleted_at IS NULL `, id) return scanAgentProfile(row) @@ -256,7 +260,7 @@ func (r *SQLiteRepository) GetAgentProfile(ctx context.Context, id string) (*mod func (r *SQLiteRepository) ListAgentProfiles(ctx context.Context, agentID string) ([]*models.AgentProfile, error) { rows, err := r.db.QueryContext(ctx, ` - SELECT id, agent_id, name, model, auto_approve, dangerously_skip_permissions, plan, created_at, updated_at, deleted_at + SELECT id, agent_id, name, agent_display_name, model, auto_approve, dangerously_skip_permissions, plan, created_at, updated_at, deleted_at FROM agent_profiles WHERE agent_id = ? AND deleted_at IS NULL ORDER BY created_at DESC `, agentID) if err != nil { @@ -311,6 +315,7 @@ func scanAgentProfile(scanner interface { &profile.ID, &profile.AgentID, &profile.Name, + &profile.AgentDisplayName, &profile.Model, &autoApprove, &skipPermissions, diff --git a/apps/backend/pkg/websocket/actions.go b/apps/backend/pkg/websocket/actions.go index 1ea873b281..a3f46b3bdc 100644 --- a/apps/backend/pkg/websocket/actions.go +++ b/apps/backend/pkg/websocket/actions.go @@ -114,6 +114,7 @@ const ( ActionTaskDeleted = "task.deleted" ActionTaskStateChanged = "task.state_changed" ActionAgentUpdated = "agent.updated" + ActionAgentAvailableUpdated = "agent.available.updated" ActionWorkspaceCreated = "workspace.created" ActionWorkspaceUpdated = "workspace.updated" ActionWorkspaceDeleted = "workspace.deleted" diff --git a/apps/web/app/s/[sessionId]/page.tsx b/apps/web/app/s/[sessionId]/page.tsx index 45d6548874..f08f85e020 100644 --- a/apps/web/app/s/[sessionId]/page.tsx +++ b/apps/web/app/s/[sessionId]/page.tsx @@ -78,7 +78,7 @@ export default async function SessionPage({ items: agents.agents.flatMap((agent) => agent.profiles.map((profile) => ({ id: profile.id, - label: `${agent.name} • ${profile.name}`, + label: `${profile.agent_display_name} • ${profile.name}`, agent_id: agent.id, })) ), diff --git a/apps/web/app/settings/agents/[agentId]/page.tsx b/apps/web/app/settings/agents/[agentId]/page.tsx index e9a4089d2e..7ddd0028d1 100644 --- a/apps/web/app/settings/agents/[agentId]/page.tsx +++ b/apps/web/app/settings/agents/[agentId]/page.tsx @@ -23,6 +23,7 @@ import { import type { Agent, AgentDiscovery, AgentProfile } from '@/lib/types/http'; import { generateUUID } from '@/lib/utils'; import { useAppStore } from '@/components/state-provider'; +import { useAvailableAgents } from '@/hooks/use-available-agents'; type DraftAgent = Agent & { isNew?: boolean }; type DraftProfile = AgentProfile & { isNew?: boolean }; @@ -34,18 +35,11 @@ type AgentSetupFormProps = { onToastError: (error: unknown) => void; }; -const AGENT_LABELS: Record = { - claude: 'Claude', - gemini: 'Gemini', - codex: 'Codex', - opencode: 'OpenCode', - copilot: 'Copilot', -}; - -const createDraftProfile = (agentId: string): DraftProfile => ({ +const createDraftProfile = (agentId: string, agentDisplayName: string): DraftProfile => ({ id: `draft-${generateUUID()}`, agent_id: agentId, name: '', + agent_display_name: agentDisplayName, model: '', auto_approve: false, dangerously_skip_permissions: false, @@ -62,13 +56,13 @@ const cloneAgent = (agent: Agent): DraftAgent => ({ profiles: agent.profiles.map((profile) => ({ ...profile })), }); -const ensureProfiles = (agent: DraftAgent): DraftAgent => { +const ensureProfiles = (agent: DraftAgent, agentDisplayName: string): DraftAgent => { if (agent.profiles.length > 0) { return agent; } return { ...agent, - profiles: [createDraftProfile(agent.id)], + profiles: [createDraftProfile(agent.id, agentDisplayName)], }; }; @@ -82,8 +76,11 @@ function AgentSetupForm({ const settingsAgents = useAppStore((state) => state.settingsAgents.items); const setSettingsAgents = useAppStore((state) => state.setSettingsAgents); const setAgentProfiles = useAppStore((state) => state.setAgentProfiles); + const availableAgents = useAvailableAgents().items; const [draftAgent, setDraftAgent] = useState(initialAgent); const [saveStatus, setSaveStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); + const resolveDisplayName = (name: string) => + availableAgents.find((item) => item.name === name)?.display_name ?? ''; const isProfileDirty = (draft: DraftProfile, saved?: AgentProfile) => { if (!saved) return true; @@ -116,7 +113,7 @@ function AgentSetupForm({ nextAgents.flatMap((agent) => agent.profiles.map((profile) => ({ id: profile.id, - label: `${agent.name} • ${profile.name}`, + label: `${profile.agent_display_name} • ${profile.name}`, agent_id: agent.id, })) ) @@ -134,7 +131,7 @@ function AgentSetupForm({ const handleAddProfile = () => { setDraftAgent((current) => ({ ...current, - profiles: [...current.profiles, createDraftProfile(current.id)], + profiles: [...current.profiles, createDraftProfile(current.id, resolveDisplayName(current.name))], })); }; @@ -143,7 +140,10 @@ function AgentSetupForm({ const remaining = current.profiles.filter((profile) => profile.id !== profileId); return { ...current, - profiles: remaining.length > 0 ? remaining : [createDraftProfile(current.id)], + profiles: + remaining.length > 0 + ? remaining + : [createDraftProfile(current.id, resolveDisplayName(current.name))], }; }); }; @@ -182,7 +182,7 @@ function AgentSetupForm({ }); } upsertAgent(created); - setDraftAgent(ensureProfiles(cloneAgent(created))); + setDraftAgent(ensureProfiles(cloneAgent(created), resolveDisplayName(created.name))); router.replace(`/settings/agents/${encodeURIComponent(created.name)}`); } else { const agentPatch: { workspace_id?: string | null; mcp_config_path?: string | null } = {}; @@ -238,7 +238,7 @@ function AgentSetupForm({ profiles: nextProfiles, }; upsertAgent(nextAgent); - setDraftAgent(ensureProfiles(cloneAgent(nextAgent))); + setDraftAgent(ensureProfiles(cloneAgent(nextAgent), resolveDisplayName(nextAgent.name))); } setSaveStatus('success'); } catch (error) { @@ -253,7 +253,7 @@ function AgentSetupForm({

- {AGENT_LABELS[draftAgent.name] ?? draftAgent.name} + {draftAgent.profiles[0]?.agent_display_name ?? draftAgent.name}

{discoveryAgent?.matched_path ?? 'Installation not detected'} @@ -270,7 +270,7 @@ function AgentSetupForm({
- {AGENT_LABELS[draftAgent.name] ?? draftAgent.name} Profiles + {draftAgent.profiles[0]?.agent_display_name ?? draftAgent.name} Profiles {isAgentDirty && }
{installedAgents.length === 0 && ( @@ -61,7 +74,7 @@ export default function AgentsSettingsPage() { )} -
+
{installedAgents.map((agent) => { const savedAgent = savedAgentsByName.get(agent.name); const configured = Boolean(savedAgent && savedAgent.profiles.length > 0); @@ -71,7 +84,9 @@ export default function AgentsSettingsPage() {
-

{AGENT_LABELS[agent.name] ?? agent.name}

+

+ {availableAgents.find((item) => item.name === agent.name)?.display_name ?? agent.name} +

{agent.supports_mcp && MCP} {configured && Configured}
@@ -110,7 +125,7 @@ export default function AgentsSettingsPage() {
- {AGENT_LABELS[agent.name] ?? agent.name} + {agent.profiles[0]?.agent_display_name ?? agent.name} {agent.supports_mcp && MCP} {profile.name} diff --git a/apps/web/app/settings/layout.tsx b/apps/web/app/settings/layout.tsx index e4e3bd987f..16fd2e3ed2 100644 --- a/apps/web/app/settings/layout.tsx +++ b/apps/web/app/settings/layout.tsx @@ -52,7 +52,7 @@ async function SettingsLayoutServer({ children }: { children: React.ReactNode }) items: agents.agents.flatMap((agent) => agent.profiles.map((profile) => ({ id: profile.id, - label: `${agent.name} • ${profile.name}`, + label: `${profile.agent_display_name} • ${profile.name}`, agent_id: agent.id, })) ), diff --git a/apps/web/components/settings/agent-card.tsx b/apps/web/components/settings/agent-card.tsx index 7aae0af74e..3e11c805d5 100644 --- a/apps/web/components/settings/agent-card.tsx +++ b/apps/web/components/settings/agent-card.tsx @@ -4,20 +4,14 @@ import Link from 'next/link'; import { IconRobot, IconChevronRight } from '@tabler/icons-react'; import { Card, CardContent } from '@kandev/ui/card'; import { Badge } from '@kandev/ui/badge'; -import type { AgentProfile, AgentType } from '@/lib/settings/types'; +import type { AgentProfile } from '@/lib/settings/types'; type AgentCardProps = { agent: AgentProfile; }; -const AGENT_LABELS: Record = { - 'claude-code': 'Claude Code', - 'codex': 'Codex', - 'auggie': 'Auggie', -}; - export function AgentCard({ agent }: AgentCardProps) { - const agentLabel = AGENT_LABELS[agent.agent] || agent.agent; + const agentLabel = agent.agentDisplayName; return ( diff --git a/apps/web/components/settings/editors-settings.tsx b/apps/web/components/settings/editors-settings.tsx index 0032a2db59..7d7ece7176 100644 --- a/apps/web/components/settings/editors-settings.tsx +++ b/apps/web/components/settings/editors-settings.tsx @@ -166,7 +166,7 @@ function EditorForm({ if (state.kind === 'custom_remote_ssh') return Boolean(state.host.trim()); if (state.kind === 'custom_hosted_url') return Boolean(state.url.trim()); return false; - }, [state.command, state.host, state.kind, state.name, state.url]); + }, [state]); return (
diff --git a/apps/web/components/settings/settings-app-sidebar.tsx b/apps/web/components/settings/settings-app-sidebar.tsx index 34fe7fc4c4..8dc6545264 100644 --- a/apps/web/components/settings/settings-app-sidebar.tsx +++ b/apps/web/components/settings/settings-app-sidebar.tsx @@ -31,6 +31,7 @@ import { } from '@kandev/ui/sidebar'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@kandev/ui/collapsible'; import { useAppStore } from '@/components/state-provider'; +import { useAvailableAgents } from '@/hooks/use-available-agents'; export function SettingsAppSidebar() { const pathname = usePathname(); @@ -38,6 +39,7 @@ export function SettingsAppSidebar() { const environments = useAppStore((state) => state.environments.items); const executors = useAppStore((state) => state.executors.items); const agents = useAppStore((state) => state.settingsAgents.items); + const availableAgents = useAvailableAgents().items; return ( @@ -276,6 +278,7 @@ export function SettingsAppSidebar() { agent.profiles.map((profile) => { const encodedAgent = encodeURIComponent(agent.name); const profilePath = `/settings/agents/${encodedAgent}/profiles/${profile.id}`; + const agentLabel = profile.agent_display_name || agent.name; return ( - {profile.name} + {agentLabel} • {profile.name} diff --git a/apps/web/components/task-preview-panel.tsx b/apps/web/components/task-preview-panel.tsx index 03d9c1ba34..79ff652ea5 100644 --- a/apps/web/components/task-preview-panel.tsx +++ b/apps/web/components/task-preview-panel.tsx @@ -14,6 +14,7 @@ interface TaskPreviewPanelProps { onMaximize?: (task: Task, sessionId: string) => void; } +// TODO: fix this const AGENTS = [ { id: "codex", label: "Codex" }, { id: "claude", label: "Claude Code" }, diff --git a/apps/web/hooks/use-available-agents.ts b/apps/web/hooks/use-available-agents.ts new file mode 100644 index 0000000000..d4df887a95 --- /dev/null +++ b/apps/web/hooks/use-available-agents.ts @@ -0,0 +1,28 @@ +import { useEffect } from 'react'; +import { useAppStore } from '@/components/state-provider'; +import { listAvailableAgents } from '@/lib/http'; + +export function useAvailableAgents(enabled = true) { + const availableAgents = useAppStore((state) => state.availableAgents); + const setAvailableAgents = useAppStore((state) => state.setAvailableAgents); + const setAvailableAgentsLoading = useAppStore((state) => state.setAvailableAgentsLoading); + + useEffect(() => { + if (!enabled) return; + if (availableAgents.loaded || availableAgents.loading) return; + setAvailableAgentsLoading(true); + listAvailableAgents({ cache: 'no-store' }) + .then((response) => { + setAvailableAgents(response.agents); + }) + .catch(() => setAvailableAgents([])); + }, [ + availableAgents.loaded, + availableAgents.loading, + enabled, + setAvailableAgents, + setAvailableAgentsLoading, + ]); + + return availableAgents; +} diff --git a/apps/web/hooks/use-settings-data.ts b/apps/web/hooks/use-settings-data.ts index a3514ad249..cad1b53282 100644 --- a/apps/web/hooks/use-settings-data.ts +++ b/apps/web/hooks/use-settings-data.ts @@ -50,7 +50,7 @@ export function useSettingsData(enabled = true) { response.agents.flatMap((agent) => agent.profiles.map((profile) => ({ id: profile.id, - label: `${agent.name} • ${profile.name}`, + label: `${profile.agent_display_name} • ${profile.name}`, agent_id: agent.id, })) ) diff --git a/apps/web/lib/http/index.ts b/apps/web/lib/http/index.ts index 25fec102c2..e8b1e4ecca 100644 --- a/apps/web/lib/http/index.ts +++ b/apps/web/lib/http/index.ts @@ -3,6 +3,7 @@ import type { BoardSnapshot, ListAgentsResponse, ListAgentDiscoveryResponse, + ListAvailableAgentsResponse, ListBoardsResponse, ListEnvironmentsResponse, ListExecutorsResponse, @@ -318,3 +319,9 @@ export async function listAgentDiscovery( ): Promise { return fetchJson('/api/v1/agents/discovery', options); } + +export async function listAvailableAgents( + options?: ApiRequestOptions +): Promise { + return fetchJson('/api/v1/agents/available', options); +} diff --git a/apps/web/lib/settings/dummy-data.ts b/apps/web/lib/settings/dummy-data.ts deleted file mode 100644 index f77718d897..0000000000 --- a/apps/web/lib/settings/dummy-data.ts +++ /dev/null @@ -1,151 +0,0 @@ -import type { SettingsData } from './types'; - -export const SETTINGS_DATA: SettingsData = { - general: { - theme: 'dark', - editor: 'vscode', - customEditorCommand: '', - notifications: { - taskUpdates: true, - agentCompletion: true, - errors: true, - }, - }, - workspaces: [ - { - id: '1', - name: 'KanDev Project', - repositories: [ - { - id: '1', - name: 'bismarck', - path: '/Users/username/projects/bismarck', - setupScript: '#!/bin/bash\nnpm install && npm run build', - cleanupScript: '#!/bin/bash\nrm -rf node_modules dist', - customScripts: [ - { id: 's1', name: 'dev', command: '#!/bin/bash\nnpm run dev' }, - { id: 's2', name: 'test', command: '#!/bin/bash\nnpm test' }, - ], - }, - { - id: '2', - name: 'backend-api', - path: '/Users/username/projects/backend-api', - setupScript: '#!/bin/bash\ngo mod download', - cleanupScript: '#!/bin/bash\ngo clean', - customScripts: [ - { id: 's3', name: 'run', command: '#!/bin/bash\ngo run main.go' }, - ], - }, - ], - contexts: [ - { - id: '1', - name: 'Architect', - columns: [ - { id: 'backlog', title: 'Backlog', color: 'bg-neutral-400' }, - { id: 'high-level-design', title: 'High Level Design', color: 'bg-cyan-500' }, - { id: 'low-level-design', title: 'Low Level Design', color: 'bg-violet-500' }, - { id: 'review', title: 'Review', color: 'bg-yellow-500' }, - { id: 'done', title: 'Done', color: 'bg-green-500' }, - ], - }, - { - id: '2', - name: 'Dev', - columns: [ - { id: 'todo', title: 'To Do', color: 'bg-neutral-400' }, - { id: 'in-progress', title: 'In Progress', color: 'bg-blue-500' }, - { id: 'review', title: 'Review', color: 'bg-yellow-500' }, - { id: 'done', title: 'Done', color: 'bg-green-500' }, - ], - }, - { - id: '3', - name: 'Team', - columns: [ - { id: 'backlog', title: 'Backlog', color: 'bg-neutral-400' }, - { id: 'solution-design', title: 'Solution Design', color: 'bg-sky-500' }, - { id: 'ready-for-dev', title: 'Ready for Dev', color: 'bg-indigo-500' }, - { id: 'in-progress', title: 'In Progress', color: 'bg-blue-500' }, - { id: 'review', title: 'Review', color: 'bg-yellow-500' }, - { id: 'done', title: 'Done', color: 'bg-green-500' }, - ], - }, - ], - }, - ], - executors: [ - { - id: 'exec-local-pc', - name: 'Local PC', - type: 'local_pc', - status: 'active', - isSystem: true, - config: {}, - }, - { - id: 'exec-local-docker', - name: 'Local Docker', - type: 'local_docker', - status: 'active', - isSystem: false, - config: { - docker_host: 'unix:///var/run/docker.sock', - }, - }, - { - id: 'exec-remote-docker', - name: 'Remote Docker', - type: 'remote_docker', - status: 'disabled', - isSystem: false, - config: {}, - }, - ], - environments: [ - { - id: 'env-local', - name: 'Local', - kind: 'local_pc', - worktreeRoot: '~/kandev', - }, - { - id: 'env-custom', - name: 'Custom Image', - kind: 'docker_image', - imageTag: 'kandev/custom:dev', - dockerfile: '', - buildConfig: { - baseImage: 'universal', - installAgents: ['claude-code', 'codex'], - }, - }, - ], - agents: [ - { - id: '1', - agent: 'claude-code', - name: 'Default Profile', - model: 'claude-sonnet-4.5', - autoApprove: false, - temperature: 0.7, - }, - { - id: '2', - agent: 'codex', - name: 'Fast Development', - model: 'codex-v2', - autoApprove: true, - temperature: 0.5, - }, - { - id: '3', - agent: 'auggie', - name: 'Code Review', - model: 'auggie-v1', - autoApprove: false, - temperature: 0.3, - }, - ], -}; diff --git a/apps/web/lib/settings/types.ts b/apps/web/lib/settings/types.ts index ea358155d3..d42caca336 100644 --- a/apps/web/lib/settings/types.ts +++ b/apps/web/lib/settings/types.ts @@ -91,6 +91,7 @@ export type AgentProfile = { id: string; agent: AgentType; name: string; + agentDisplayName: string; model: string; autoApprove: boolean; temperature: number; diff --git a/apps/web/lib/state/store.ts b/apps/web/lib/state/store.ts index 9e080e21d8..15aef835fb 100644 --- a/apps/web/lib/state/store.ts +++ b/apps/web/lib/state/store.ts @@ -1,6 +1,7 @@ import type { ReactNode } from 'react'; import type { Agent, + AvailableAgent, AgentDiscovery, Branch, Environment, @@ -69,6 +70,12 @@ export type EditorsState = { items: EditorOption[]; loaded: boolean; loading: boolean; +} + +export type AvailableAgentsState = { + items: AvailableAgent[]; + loading: boolean; + loaded: boolean; }; export type BoardState = { @@ -229,6 +236,7 @@ export type AppState = { environments: EnvironmentsState; settingsAgents: SettingsAgentsState; agentDiscovery: AgentDiscoveryState; + availableAgents: AvailableAgentsState; repositories: RepositoriesState; repositoryBranches: RepositoryBranchesState; settingsData: SettingsDataState; @@ -257,6 +265,8 @@ export type AppState = { setEnvironments: (environments: EnvironmentsState['items']) => void; setSettingsAgents: (agents: SettingsAgentsState['items']) => void; setAgentDiscovery: (agents: AgentDiscoveryState['items']) => void; + setAvailableAgents: (agents: AvailableAgentsState['items']) => void; + setAvailableAgentsLoading: (loading: boolean) => void; setAgentProfiles: (profiles: AgentProfilesState['items']) => void; setRepositories: (workspaceId: string, repositories: Repository[]) => void; setRepositoriesLoading: (workspaceId: string, loading: boolean) => void; @@ -315,6 +325,7 @@ const defaultState: AppState = { environments: { items: [] }, settingsAgents: { items: [] }, agentDiscovery: { items: [] }, + availableAgents: { items: [], loading: false, loaded: false }, repositories: { itemsByWorkspaceId: {}, loadingByWorkspaceId: {}, loadedByWorkspaceId: {} }, repositoryBranches: { itemsByRepositoryId: {}, loadingByRepositoryId: {}, loadedByRepositoryId: {} }, settingsData: { executorsLoaded: false, environmentsLoaded: false, agentsLoaded: false }, @@ -350,6 +361,8 @@ const defaultState: AppState = { setEnvironments: () => undefined, setSettingsAgents: () => undefined, setAgentDiscovery: () => undefined, + setAvailableAgents: () => undefined, + setAvailableAgentsLoading: () => undefined, setAgentProfiles: () => undefined, setRepositories: () => undefined, setRepositoriesLoading: () => undefined, @@ -397,6 +410,8 @@ function mergeInitialState( | 'setEnvironments' | 'setSettingsAgents' | 'setAgentDiscovery' + | 'setAvailableAgents' + | 'setAvailableAgentsLoading' | 'setAgentProfiles' | 'setRepositories' | 'setRepositoriesLoading' @@ -438,6 +453,7 @@ function mergeInitialState( environments: { ...defaultState.environments, ...initialState.environments }, settingsAgents: { ...defaultState.settingsAgents, ...initialState.settingsAgents }, agentDiscovery: { ...defaultState.agentDiscovery, ...initialState.agentDiscovery }, + availableAgents: { ...defaultState.availableAgents, ...initialState.availableAgents }, repositories: { ...defaultState.repositories, ...initialState.repositories }, repositoryBranches: { ...defaultState.repositoryBranches, ...initialState.repositoryBranches }, settingsData: { ...defaultState.settingsData, ...initialState.settingsData }, @@ -478,6 +494,7 @@ export function createAppStore(initialState?: Partial) { if (state.environments) Object.assign(draft.environments, state.environments); if (state.settingsAgents) Object.assign(draft.settingsAgents, state.settingsAgents); if (state.agentDiscovery) Object.assign(draft.agentDiscovery, state.agentDiscovery); + if (state.availableAgents) Object.assign(draft.availableAgents, state.availableAgents); if (state.repositories) Object.assign(draft.repositories, state.repositories); if (state.repositoryBranches) Object.assign(draft.repositoryBranches, state.repositoryBranches); if (state.settingsData) Object.assign(draft.settingsData, state.settingsData); @@ -543,6 +560,19 @@ export function createAppStore(initialState?: Partial) { set((draft) => { draft.agentDiscovery.items = agents; }), + setAvailableAgents: (agents) => + set((draft) => { + draft.availableAgents.items = agents; + draft.availableAgents.loading = false; + draft.availableAgents.loaded = true; + }), + setAvailableAgentsLoading: (loading) => + set((draft) => { + draft.availableAgents.loading = loading; + if (!loading && !draft.availableAgents.loaded) { + draft.availableAgents.loaded = false; + } + }), setAgentProfiles: (profiles) => set((draft) => { draft.agentProfiles.items = profiles; diff --git a/apps/web/lib/types/backend.ts b/apps/web/lib/types/backend.ts index 52f9f95319..782ac5b71b 100644 --- a/apps/web/lib/types/backend.ts +++ b/apps/web/lib/types/backend.ts @@ -5,6 +5,7 @@ export type BackendMessageType = | 'task.deleted' | 'task.state_changed' | 'agent.updated' + | 'agent.available.updated' | 'terminal.output' | 'diff.update' | 'session.git.status' @@ -46,7 +47,7 @@ export type BackendMessage = { timestamp?: string; }; -import type { TaskState } from '@/lib/types/http'; +import type { AvailableAgent, TaskState } from '@/lib/types/http'; export type KanbanUpdatePayload = { boardId: string; @@ -79,6 +80,10 @@ export type AgentUpdatePayload = { message?: string; }; +export type AgentAvailableUpdatedPayload = { + agents: AvailableAgent[]; +}; + export type TerminalOutputPayload = { terminalId: string; data: string; @@ -219,6 +224,7 @@ export type AgentProfilePayload = { id: string; agent_id: string; name: string; + agent_display_name: string; model: string; auto_approve: boolean; dangerously_skip_permissions: boolean; @@ -261,6 +267,7 @@ export type BackendMessageMap = { 'task.deleted': BackendMessage<'task.deleted', TaskEventPayload>; 'task.state_changed': BackendMessage<'task.state_changed', TaskEventPayload>; 'agent.updated': BackendMessage<'agent.updated', AgentUpdatePayload>; + 'agent.available.updated': BackendMessage<'agent.available.updated', AgentAvailableUpdatedPayload>; 'terminal.output': BackendMessage<'terminal.output', TerminalOutputPayload>; 'diff.update': BackendMessage<'diff.update', DiffUpdatePayload>; 'session.git.status': BackendMessage<'session.git.status', GitStatusPayload>; diff --git a/apps/web/lib/types/http.ts b/apps/web/lib/types/http.ts index 4b958ff6bd..8d4a713211 100644 --- a/apps/web/lib/types/http.ts +++ b/apps/web/lib/types/http.ts @@ -341,6 +341,7 @@ export type AgentProfile = { id: string; agent_id: string; name: string; + agent_display_name: string; model: string; auto_approve: boolean; dangerously_skip_permissions: boolean; @@ -369,6 +370,24 @@ export type AgentDiscovery = { matched_path?: string | null; }; +export type AgentCapabilities = { + supports_session_resume: boolean; + supports_shell: boolean; + supports_workspace_only: boolean; +}; + +export type AvailableAgent = { + name: string; + display_name: string; + supports_mcp: boolean; + mcp_config_path?: string | null; + installation_paths: string[]; + available: boolean; + matched_path?: string | null; + capabilities: AgentCapabilities; + updated_at: string; +}; + export type ListAgentsResponse = { agents: Agent[]; total: number; @@ -378,3 +397,8 @@ export type ListAgentDiscoveryResponse = { agents: AgentDiscovery[]; total: number; }; + +export type ListAvailableAgentsResponse = { + agents: AvailableAgent[]; + total: number; +}; diff --git a/apps/web/lib/ws/handlers/agents.ts b/apps/web/lib/ws/handlers/agents.ts index 31058ee7cd..d36497ad00 100644 --- a/apps/web/lib/ws/handlers/agents.ts +++ b/apps/web/lib/ws/handlers/agents.ts @@ -1,9 +1,17 @@ import type { StoreApi } from 'zustand'; import type { AppState } from '@/lib/state/store'; import type { WsHandlers } from '@/lib/ws/handlers/types'; - export function registerAgentsHandlers(store: StoreApi): WsHandlers { return { + 'agent.available.updated': (message) => { + store.setState((state) => { + const availableAgents = message.payload.agents ?? []; + return { + ...state, + availableAgents: { items: availableAgents, loaded: true, loading: false }, + }; + }); + }, 'agent.updated': (message) => { store.setState((state) => ({ ...state, @@ -20,10 +28,7 @@ export function registerAgentsHandlers(store: StoreApi): WsHandlers { }, 'agent.profile.created': (message) => { store.setState((state) => { - const agent = state.settingsAgents.items.find( - (item) => item.id === message.payload.profile.agent_id - ); - const label = agent ? `${agent.name} • ${message.payload.profile.name}` : message.payload.profile.name; + const label = `${message.payload.profile.agent_display_name} • ${message.payload.profile.name}`; const nextProfiles = [ ...state.agentProfiles.items.filter((profile) => profile.id !== message.payload.profile.id), { id: message.payload.profile.id, label, agent_id: message.payload.profile.agent_id }, @@ -38,6 +43,7 @@ export function registerAgentsHandlers(store: StoreApi): WsHandlers { id: message.payload.profile.id, agent_id: message.payload.profile.agent_id, name: message.payload.profile.name, + agent_display_name: message.payload.profile.agent_display_name, model: message.payload.profile.model, auto_approve: message.payload.profile.auto_approve, dangerously_skip_permissions: message.payload.profile.dangerously_skip_permissions, @@ -58,10 +64,7 @@ export function registerAgentsHandlers(store: StoreApi): WsHandlers { }, 'agent.profile.updated': (message) => { store.setState((state) => { - const agent = state.settingsAgents.items.find( - (item) => item.id === message.payload.profile.agent_id - ); - const label = agent ? `${agent.name} • ${message.payload.profile.name}` : message.payload.profile.name; + const label = `${message.payload.profile.agent_display_name} • ${message.payload.profile.name}`; const nextProfiles = state.agentProfiles.items.map((profile) => profile.id === message.payload.profile.id ? { ...profile, label, agent_id: message.payload.profile.agent_id } @@ -76,6 +79,7 @@ export function registerAgentsHandlers(store: StoreApi): WsHandlers { ? { ...profile, name: message.payload.profile.name, + agent_display_name: message.payload.profile.agent_display_name, model: message.payload.profile.model, auto_approve: message.payload.profile.auto_approve, dangerously_skip_permissions: message.payload.profile.dangerously_skip_permissions, diff --git a/docs/asyncapi.yaml b/docs/asyncapi.yaml index fa1218d55d..3e83689e9b 100644 --- a/docs/asyncapi.yaml +++ b/docs/asyncapi.yaml @@ -181,6 +181,7 @@ components: - acp.heartbeat - task.updated - agent.updated + - agent.available.updated payload: type: object description: Notification data @@ -405,4 +406,4 @@ components: type: type: string path: - type: string \ No newline at end of file + type: string