feat: improve list agents
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
),
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
),
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
}))
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -91,6 +91,7 @@ export type AgentProfile = {
|
||||
id: string;
|
||||
agent: AgentType;
|
||||
name: string;
|
||||
agentDisplayName: string;
|
||||
model: string;
|
||||
autoApprove: boolean;
|
||||
temperature: number;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user