feat: improve list agents

This commit is contained in:
Carlos Florêncio
2026-01-19 00:16:38 +00:00
parent 9adf40e0cc
commit c769e69ed7
27 changed files with 389 additions and 324 deletions
@@ -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
}
}
]
@@ -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) {
@@ -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 ""
}
@@ -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"`
}
@@ -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)
}
@@ -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"`
}
@@ -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,
+1
View File
@@ -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"
+1 -1
View File
@@ -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,
}))
),
+24 -21
View File
@@ -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<string, string> = {
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<DraftAgent>(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({
<div>
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-2xl font-bold">
{AGENT_LABELS[draftAgent.name] ?? draftAgent.name}
{draftAgent.profiles[0]?.agent_display_name ?? draftAgent.name}
</h2>
<span className="text-xs text-muted-foreground border border-muted-foreground/30 rounded-full px-2 py-1">
{discoveryAgent?.matched_path ?? 'Installation not detected'}
@@ -270,7 +270,7 @@ function AgentSetupForm({
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div className="flex items-center gap-2">
<CardTitle>{AGENT_LABELS[draftAgent.name] ?? draftAgent.name} Profiles</CardTitle>
<CardTitle>{draftAgent.profiles[0]?.agent_display_name ?? draftAgent.name} Profiles</CardTitle>
{isAgentDirty && <UnsavedChangesBadge />}
</div>
<Button size="sm" variant="outline" onClick={handleAddProfile}>
@@ -386,6 +386,7 @@ export default function AgentSetupPage() {
const decodedKey = decodeURIComponent(agentKey ?? '');
const discoveryAgents = useAppStore((state) => state.agentDiscovery.items);
const savedAgents = useAppStore((state) => state.settingsAgents.items);
const availableAgents = useAvailableAgents().items;
const discoveryAgent = useMemo(() => {
return discoveryAgents.find((agent) => agent.name === decodedKey);
@@ -397,8 +398,10 @@ export default function AgentSetupPage() {
const initialAgent = useMemo(() => {
if (!decodedKey) return null;
const resolveDisplayName = (name: string) =>
availableAgents.find((item) => item.name === name)?.display_name ?? '';
if (savedAgent) {
return ensureProfiles(cloneAgent(savedAgent));
return ensureProfiles(cloneAgent(savedAgent), resolveDisplayName(savedAgent.name));
}
if (discoveryAgent) {
const draft: DraftAgent = {
@@ -412,10 +415,10 @@ export default function AgentSetupPage() {
updated_at: new Date().toISOString(),
isNew: true,
};
return ensureProfiles(draft);
return ensureProfiles(draft, resolveDisplayName(draft.name));
}
return null;
}, [decodedKey, discoveryAgent, savedAgent]);
}, [decodedKey, discoveryAgent, savedAgent, availableAgents]);
if (!initialAgent && discoveryAgents.length > 0) {
return (
@@ -18,14 +18,6 @@ import { deleteAgentProfileAction, updateAgentProfileAction } from '@/app/action
import type { Agent, AgentProfile } from '@/lib/types/http';
import { useAppStore } from '@/components/state-provider';
const AGENT_LABELS: Record<string, string> = {
claude: 'Claude',
gemini: 'Gemini',
codex: 'Codex',
opencode: 'OpenCode',
copilot: 'Copilot',
};
type ProfileEditorProps = {
agent: Agent;
profile: AgentProfile;
@@ -45,7 +37,7 @@ function ProfileEditor({ agent, profile }: ProfileEditorProps) {
nextAgents.flatMap((agentItem) =>
agentItem.profiles.map((agentProfile) => ({
id: agentProfile.id,
label: `${agentItem.name} • ${agentProfile.name}`,
label: `${agentProfile.agent_display_name} • ${agentProfile.name}`,
agent_id: agentItem.id,
}))
)
@@ -77,11 +69,11 @@ function ProfileEditor({ agent, profile }: ProfileEditorProps) {
const nextAgents = settingsAgents.map((agentItem) =>
agentItem.id === agent.id
? {
...agentItem,
profiles: agentItem.profiles.map((profileItem) =>
profileItem.id === updated.id ? updated : profileItem
),
}
...agentItem,
profiles: agentItem.profiles.map((profileItem) =>
profileItem.id === updated.id ? updated : profileItem
),
}
: agentItem
);
syncAgentsToStore(nextAgents);
@@ -102,9 +94,9 @@ function ProfileEditor({ agent, profile }: ProfileEditorProps) {
const nextAgents = settingsAgents.map((agentItem) =>
agentItem.id === agent.id
? {
...agentItem,
profiles: agentItem.profiles.filter((profileItem) => profileItem.id !== draft.id),
}
...agentItem,
profiles: agentItem.profiles.filter((profileItem) => profileItem.id !== draft.id),
}
: agentItem
);
syncAgentsToStore(nextAgents);
@@ -122,9 +114,9 @@ function ProfileEditor({ agent, profile }: ProfileEditorProps) {
<div className="space-y-8">
<div className="flex items-start justify-between">
<div>
<h2 className="text-2xl font-bold">{savedProfile.name}</h2>
<h2 className="text-2xl font-bold">{profile.agent_display_name} • {savedProfile.name}</h2>
<p className="text-sm text-muted-foreground mt-1">
{AGENT_LABELS[agent.name] ?? agent.name} profile settings
{profile.agent_display_name} profile settings
</p>
</div>
<div className="flex items-center gap-3">
+32 -17
View File
@@ -1,6 +1,6 @@
'use client';
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import Link from 'next/link';
import { IconAlertTriangle, IconSettings } from '@tabler/icons-react';
import { Badge } from '@kandev/ui/badge';
@@ -8,18 +8,15 @@ 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';
const AGENT_LABELS: Record<string, string> = {
claude: 'Claude',
gemini: 'Gemini',
codex: 'Codex',
opencode: 'OpenCode',
copilot: 'Copilot',
};
import { listAgentDiscovery } from '@/lib/http';
import { useAvailableAgents } from '@/hooks/use-available-agents';
export default function AgentsSettingsPage() {
const discoveryAgents = useAppStore((state) => state.agentDiscovery.items);
const savedAgents = useAppStore((state) => state.settingsAgents.items);
const setAgentDiscovery = useAppStore((state) => state.setAgentDiscovery);
const availableAgents = useAvailableAgents().items;
const [rescanning, setRescanning] = useState(false);
const installedAgents = useMemo(
() => discoveryAgents.filter((agent) => agent.available),
@@ -31,6 +28,17 @@ export default function AgentsSettingsPage() {
[savedAgents]
);
const handleRescan = async () => {
if (rescanning) return;
setRescanning(true);
try {
const response = await listAgentDiscovery({ cache: 'no-store' });
setAgentDiscovery(response.agents);
} finally {
setRescanning(false);
}
};
return (
<div className="space-y-8">
<div>
@@ -43,11 +51,16 @@ export default function AgentsSettingsPage() {
<Separator />
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold">Local Agents Found</h3>
<p className="text-sm text-muted-foreground">
Agents detected on this machine are ready to configure.
</p>
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="text-lg font-semibold">Supported agents found</h3>
<p className="text-sm text-muted-foreground">
Agents detected on this machine are ready to configure.
</p>
</div>
<Button variant="outline" size="sm" onClick={handleRescan} disabled={rescanning}>
{rescanning ? 'Rescanning…' : 'Rescan'}
</Button>
</div>
{installedAgents.length === 0 && (
@@ -61,7 +74,7 @@ export default function AgentsSettingsPage() {
</Card>
)}
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
{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() {
<CardContent className="py-4 flex flex-col gap-3">
<div className="space-y-1">
<div className="flex items-center gap-2">
<h4 className="font-medium">{AGENT_LABELS[agent.name] ?? agent.name}</h4>
<h4 className="font-medium">
{availableAgents.find((item) => item.name === agent.name)?.display_name ?? agent.name}
</h4>
{agent.supports_mcp && <Badge variant="secondary">MCP</Badge>}
{configured && <Badge variant="outline">Configured</Badge>}
</div>
@@ -110,7 +125,7 @@ export default function AgentsSettingsPage() {
<CardContent className="py-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">
{AGENT_LABELS[agent.name] ?? agent.name}
{agent.profiles[0]?.agent_display_name ?? agent.name}
</span>
{agent.supports_mcp && <Badge variant="secondary">MCP</Badge>}
<span className="text-sm text-muted-foreground">{profile.name}</span>
+1 -1
View File
@@ -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,
}))
),
+2 -8
View File
@@ -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<AgentType, string> = {
'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 (
<Link href={`/settings/agents/${agent.id}`}>
@@ -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 (
<div className="rounded-lg border border-border/70 bg-background p-4 space-y-4">
@@ -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 (
<Sidebar variant="inset">
@@ -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 (
<SidebarMenuSubItem key={profile.id}>
<SidebarMenuSubButton
@@ -283,7 +286,7 @@ export function SettingsAppSidebar() {
isActive={pathname === profilePath}
>
<Link href={profilePath}>
<span>{profile.name}</span>
<span>{agentLabel} • {profile.name}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
@@ -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" },
+28
View File
@@ -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;
}
+1 -1
View File
@@ -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,
}))
)
+7
View File
@@ -3,6 +3,7 @@ import type {
BoardSnapshot,
ListAgentsResponse,
ListAgentDiscoveryResponse,
ListAvailableAgentsResponse,
ListBoardsResponse,
ListEnvironmentsResponse,
ListExecutorsResponse,
@@ -318,3 +319,9 @@ export async function listAgentDiscovery(
): Promise<ListAgentDiscoveryResponse> {
return fetchJson<ListAgentDiscoveryResponse>('/api/v1/agents/discovery', options);
}
export async function listAvailableAgents(
options?: ApiRequestOptions
): Promise<ListAvailableAgentsResponse> {
return fetchJson<ListAvailableAgentsResponse>('/api/v1/agents/available', options);
}
-151
View File
@@ -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,
},
],
};
+1
View File
@@ -91,6 +91,7 @@ export type AgentProfile = {
id: string;
agent: AgentType;
name: string;
agentDisplayName: string;
model: string;
autoApprove: boolean;
temperature: number;
+30
View File
@@ -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<AppState>) {
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<AppState>) {
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;
+8 -1
View File
@@ -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<T extends BackendMessageType, P> = {
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>;
+24
View File
@@ -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;
};
+13 -9
View File
@@ -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<AppState>): 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<AppState>): 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<AppState>): 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<AppState>): 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<AppState>): 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,
+2 -1
View File
@@ -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
type: string