fix: agent profile creation (#170)
This commit is contained in:
@@ -208,7 +208,6 @@ func (a *lifecycleAdapter) ResolveAgentProfile(ctx context.Context, profileID st
|
||||
Model: info.Model,
|
||||
AutoApprove: info.AutoApprove,
|
||||
DangerouslySkipPermissions: info.DangerouslySkipPermissions,
|
||||
Plan: info.Plan,
|
||||
CLIPassthrough: info.CLIPassthrough,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -737,9 +737,6 @@ func (m *Manager) Launch(ctx context.Context, req *LaunchRequest) (*AgentExecuti
|
||||
if profileInfo.AutoApprove {
|
||||
reqWithWorktree.Env["AGENTCTL_AUTO_APPROVE_PERMISSIONS"] = "true"
|
||||
}
|
||||
if profileInfo.Plan != "" {
|
||||
reqWithWorktree.Env["AGENT_PLAN"] = profileInfo.Plan
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Launch via runtime - creates agentctl instance with workspace access only
|
||||
|
||||
@@ -53,6 +53,5 @@ func (r *StoreProfileResolver) ResolveProfile(ctx context.Context, profileID str
|
||||
DangerouslySkipPermissions: profile.DangerouslySkipPermissions,
|
||||
AllowIndexing: profile.AllowIndexing,
|
||||
CLIPassthrough: profile.CLIPassthrough,
|
||||
Plan: profile.Plan,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -116,7 +116,6 @@ func TestStoreProfileResolver_ResolveProfile_Success(t *testing.T) {
|
||||
Model: "claude-3.5-sonnet",
|
||||
AutoApprove: true,
|
||||
DangerouslySkipPermissions: false,
|
||||
Plan: "pro",
|
||||
}, nil
|
||||
},
|
||||
GetAgentFn: func(ctx context.Context, id string) (*models.Agent, error) {
|
||||
@@ -159,9 +158,6 @@ func TestStoreProfileResolver_ResolveProfile_Success(t *testing.T) {
|
||||
if info.DangerouslySkipPermissions != false {
|
||||
t.Error("expected DangerouslySkipPermissions to be false")
|
||||
}
|
||||
if info.Plan != "pro" {
|
||||
t.Errorf("expected Plan 'pro', got '%s'", info.Plan)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreProfileResolver_ResolveProfile_ProfileNotFound(t *testing.T) {
|
||||
|
||||
@@ -155,7 +155,6 @@ type AgentProfileInfo struct {
|
||||
DangerouslySkipPermissions bool
|
||||
AllowIndexing bool
|
||||
CLIPassthrough bool
|
||||
Plan string
|
||||
}
|
||||
|
||||
// ProfileResolver resolves agent profile IDs to profile information
|
||||
|
||||
@@ -317,7 +317,6 @@ func (c *Controller) EnsureInitialAgentProfiles(ctx context.Context) error {
|
||||
AgentID: agent.ID,
|
||||
Name: defaultAgentProfileName,
|
||||
Model: defaultModel,
|
||||
Plan: "",
|
||||
AgentDisplayName: displayName,
|
||||
AutoApprove: autoApprove,
|
||||
AllowIndexing: allowIndexing,
|
||||
@@ -405,7 +404,6 @@ type CreateAgentProfileRequest struct {
|
||||
Model string
|
||||
AutoApprove bool
|
||||
DangerouslySkipPermissions bool
|
||||
Plan string
|
||||
}
|
||||
|
||||
func (c *Controller) CreateAgent(ctx context.Context, req CreateAgentRequest) (*dto.AgentDTO, error) {
|
||||
@@ -458,7 +456,6 @@ func (c *Controller) CreateAgent(ctx context.Context, req CreateAgentRequest) (*
|
||||
Model: profileReq.Model,
|
||||
AutoApprove: profileReq.AutoApprove,
|
||||
DangerouslySkipPermissions: profileReq.DangerouslySkipPermissions,
|
||||
Plan: profileReq.Plan,
|
||||
}
|
||||
if err := c.repo.CreateAgentProfile(ctx, profile); err != nil {
|
||||
return nil, err
|
||||
@@ -519,7 +516,6 @@ type CreateProfileRequest struct {
|
||||
DangerouslySkipPermissions bool
|
||||
AllowIndexing bool
|
||||
CLIPassthrough bool
|
||||
Plan string
|
||||
}
|
||||
|
||||
func (c *Controller) CreateProfile(ctx context.Context, req CreateProfileRequest) (*dto.AgentProfileDTO, error) {
|
||||
@@ -544,7 +540,6 @@ func (c *Controller) CreateProfile(ctx context.Context, req CreateProfileRequest
|
||||
DangerouslySkipPermissions: req.DangerouslySkipPermissions,
|
||||
AllowIndexing: req.AllowIndexing,
|
||||
CLIPassthrough: req.CLIPassthrough,
|
||||
Plan: req.Plan,
|
||||
}
|
||||
if err := c.repo.CreateAgentProfile(ctx, profile); err != nil {
|
||||
return nil, err
|
||||
@@ -561,7 +556,6 @@ type UpdateProfileRequest struct {
|
||||
DangerouslySkipPermissions *bool
|
||||
AllowIndexing *bool
|
||||
CLIPassthrough *bool
|
||||
Plan *string
|
||||
}
|
||||
|
||||
func (c *Controller) UpdateProfile(ctx context.Context, req UpdateProfileRequest) (*dto.AgentProfileDTO, error) {
|
||||
@@ -591,9 +585,6 @@ func (c *Controller) UpdateProfile(ctx context.Context, req UpdateProfileRequest
|
||||
if req.CLIPassthrough != nil {
|
||||
profile.CLIPassthrough = *req.CLIPassthrough
|
||||
}
|
||||
if req.Plan != nil {
|
||||
profile.Plan = *req.Plan
|
||||
}
|
||||
if err := c.repo.UpdateAgentProfile(ctx, profile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -656,7 +647,6 @@ func toProfileDTO(profile *models.AgentProfile) dto.AgentProfileDTO {
|
||||
DangerouslySkipPermissions: profile.DangerouslySkipPermissions,
|
||||
AllowIndexing: profile.AllowIndexing,
|
||||
CLIPassthrough: profile.CLIPassthrough,
|
||||
Plan: profile.Plan,
|
||||
CreatedAt: profile.CreatedAt,
|
||||
UpdatedAt: profile.UpdatedAt,
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ type AgentProfileDTO struct {
|
||||
DangerouslySkipPermissions bool `json:"dangerously_skip_permissions"`
|
||||
AllowIndexing bool `json:"allow_indexing"`
|
||||
CLIPassthrough bool `json:"cli_passthrough"`
|
||||
Plan string `json:"plan"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@ type createAgentProfileRequest struct {
|
||||
Model string `json:"model"`
|
||||
AutoApprove bool `json:"auto_approve"`
|
||||
DangerouslySkipPermissions bool `json:"dangerously_skip_permissions"`
|
||||
Plan string `json:"plan"`
|
||||
}
|
||||
|
||||
func (h *Handlers) httpCreateAgent(c *gin.Context) {
|
||||
@@ -130,7 +129,6 @@ func (h *Handlers) httpCreateAgent(c *gin.Context) {
|
||||
Model: profile.Model,
|
||||
AutoApprove: profile.AutoApprove,
|
||||
DangerouslySkipPermissions: profile.DangerouslySkipPermissions,
|
||||
Plan: profile.Plan,
|
||||
})
|
||||
}
|
||||
resp, err := h.controller.CreateAgent(c.Request.Context(), controller.CreateAgentRequest{
|
||||
@@ -286,7 +284,6 @@ type createProfileRequest struct {
|
||||
DangerouslySkipPermissions bool `json:"dangerously_skip_permissions"`
|
||||
AllowIndexing bool `json:"allow_indexing"`
|
||||
CLIPassthrough bool `json:"cli_passthrough"`
|
||||
Plan string `json:"plan"`
|
||||
}
|
||||
|
||||
func (h *Handlers) httpCreateProfile(c *gin.Context) {
|
||||
@@ -307,7 +304,6 @@ func (h *Handlers) httpCreateProfile(c *gin.Context) {
|
||||
DangerouslySkipPermissions: body.DangerouslySkipPermissions,
|
||||
AllowIndexing: body.AllowIndexing,
|
||||
CLIPassthrough: body.CLIPassthrough,
|
||||
Plan: body.Plan,
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("failed to create profile", zap.Error(err))
|
||||
@@ -330,7 +326,6 @@ type updateProfileRequest struct {
|
||||
DangerouslySkipPermissions *bool `json:"dangerously_skip_permissions,omitempty"`
|
||||
AllowIndexing *bool `json:"allow_indexing,omitempty"`
|
||||
CLIPassthrough *bool `json:"cli_passthrough,omitempty"`
|
||||
Plan *string `json:"plan,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Handlers) httpUpdateProfile(c *gin.Context) {
|
||||
@@ -351,7 +346,6 @@ func (h *Handlers) httpUpdateProfile(c *gin.Context) {
|
||||
DangerouslySkipPermissions: body.DangerouslySkipPermissions,
|
||||
AllowIndexing: body.AllowIndexing,
|
||||
CLIPassthrough: body.CLIPassthrough,
|
||||
Plan: body.Plan,
|
||||
})
|
||||
if err != nil {
|
||||
if err == controller.ErrAgentProfileNotFound {
|
||||
|
||||
@@ -22,7 +22,6 @@ type AgentProfile struct {
|
||||
DangerouslySkipPermissions bool `json:"dangerously_skip_permissions"`
|
||||
AllowIndexing bool `json:"allow_indexing"`
|
||||
CLIPassthrough bool `json:"cli_passthrough"`
|
||||
Plan string `json:"plan"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
|
||||
@@ -253,9 +253,9 @@ func (r *sqliteRepository) CreateAgentProfile(ctx context.Context, profile *mode
|
||||
profile.UpdatedAt = now
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO agent_profiles (id, agent_id, name, agent_display_name, model, auto_approve, dangerously_skip_permissions, allow_indexing, cli_passthrough, plan, created_at, updated_at, deleted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?, ?, ?)
|
||||
`, profile.ID, profile.AgentID, profile.Name, profile.AgentDisplayName, profile.Model, sqlite.BoolToInt(profile.AutoApprove),
|
||||
sqlite.BoolToInt(profile.DangerouslySkipPermissions), sqlite.BoolToInt(profile.AllowIndexing), sqlite.BoolToInt(profile.CLIPassthrough), profile.Plan, profile.CreatedAt, profile.UpdatedAt, profile.DeletedAt)
|
||||
sqlite.BoolToInt(profile.DangerouslySkipPermissions), sqlite.BoolToInt(profile.AllowIndexing), sqlite.BoolToInt(profile.CLIPassthrough), profile.CreatedAt, profile.UpdatedAt, profile.DeletedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -263,10 +263,10 @@ 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 = ?, agent_display_name = ?, model = ?, auto_approve = ?, dangerously_skip_permissions = ?, allow_indexing = ?, cli_passthrough = ?, plan = ?, updated_at = ?
|
||||
SET name = ?, agent_display_name = ?, model = ?, auto_approve = ?, dangerously_skip_permissions = ?, allow_indexing = ?, cli_passthrough = ?, updated_at = ?
|
||||
WHERE id = ? AND deleted_at IS NULL
|
||||
`, profile.Name, profile.AgentDisplayName, profile.Model, sqlite.BoolToInt(profile.AutoApprove),
|
||||
sqlite.BoolToInt(profile.DangerouslySkipPermissions), sqlite.BoolToInt(profile.AllowIndexing), sqlite.BoolToInt(profile.CLIPassthrough), profile.Plan, profile.UpdatedAt, profile.ID)
|
||||
sqlite.BoolToInt(profile.DangerouslySkipPermissions), sqlite.BoolToInt(profile.AllowIndexing), sqlite.BoolToInt(profile.CLIPassthrough), profile.UpdatedAt, profile.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -355,6 +355,7 @@ func scanAgentProfile(scanner interface {
|
||||
var skipPermissions int
|
||||
var allowIndexing int
|
||||
var cliPassthrough int
|
||||
var plan string // unused, kept for backwards compatibility
|
||||
if err := scanner.Scan(
|
||||
&profile.ID,
|
||||
&profile.AgentID,
|
||||
@@ -365,7 +366,7 @@ func scanAgentProfile(scanner interface {
|
||||
&skipPermissions,
|
||||
&allowIndexing,
|
||||
&cliPassthrough,
|
||||
&profile.Plan,
|
||||
&plan,
|
||||
&profile.CreatedAt,
|
||||
&profile.UpdatedAt,
|
||||
&profile.DeletedAt,
|
||||
|
||||
@@ -69,7 +69,6 @@ type AgentProfileInfo struct {
|
||||
Model string
|
||||
AutoApprove bool
|
||||
DangerouslySkipPermissions bool
|
||||
Plan string
|
||||
CLIPassthrough bool
|
||||
}
|
||||
|
||||
@@ -261,7 +260,6 @@ func (e *Executor) PrepareSession(ctx context.Context, task *v1.Task, agentProfi
|
||||
"model": profileInfo.Model,
|
||||
"auto_approve": profileInfo.AutoApprove,
|
||||
"dangerously_skip_permissions": profileInfo.DangerouslySkipPermissions,
|
||||
"plan": profileInfo.Plan,
|
||||
"cli_passthrough": profileInfo.CLIPassthrough,
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -51,7 +51,7 @@ export async function createAgentAction(payload: {
|
||||
auto_approve: boolean;
|
||||
dangerously_skip_permissions: boolean;
|
||||
allow_indexing: boolean;
|
||||
plan: string;
|
||||
cli_passthrough: boolean;
|
||||
}>;
|
||||
}): Promise<Agent> {
|
||||
return fetchJson<Agent>(`${apiBaseUrl}/api/v1/agents`, {
|
||||
@@ -83,7 +83,6 @@ export async function createAgentProfileAction(
|
||||
dangerously_skip_permissions: boolean;
|
||||
allow_indexing: boolean;
|
||||
cli_passthrough: boolean;
|
||||
plan: string;
|
||||
}
|
||||
): Promise<AgentProfile> {
|
||||
return fetchJson<AgentProfile>(`${apiBaseUrl}/api/v1/agents/${agentId}/profiles`, {
|
||||
@@ -94,7 +93,7 @@ export async function createAgentProfileAction(
|
||||
|
||||
export async function updateAgentProfileAction(
|
||||
id: string,
|
||||
payload: Partial<Pick<AgentProfile, 'name' | 'model' | 'auto_approve' | 'dangerously_skip_permissions' | 'allow_indexing' | 'cli_passthrough' | 'plan'>>
|
||||
payload: Partial<Pick<AgentProfile, 'name' | 'model' | 'auto_approve' | 'dangerously_skip_permissions' | 'allow_indexing' | 'cli_passthrough'>>
|
||||
): Promise<AgentProfile> {
|
||||
return fetchJson<AgentProfile>(`${apiBaseUrl}/api/v1/agent-profiles/${id}`, {
|
||||
method: 'PATCH',
|
||||
|
||||
@@ -2,24 +2,14 @@
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { IconPlus } from '@tabler/icons-react';
|
||||
import { Button } from '@kandev/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kandev/ui/card';
|
||||
import { Input } from '@kandev/ui/input';
|
||||
import { Label } from '@kandev/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@kandev/ui/select';
|
||||
import { Separator } from '@kandev/ui/separator';
|
||||
import { Switch } from '@kandev/ui/switch';
|
||||
import { Textarea } from '@kandev/ui/textarea';
|
||||
import { useToast } from '@/components/toast-provider';
|
||||
import { UnsavedChangesBadge, UnsavedSaveButton } from '@/components/settings/unsaved-indicator';
|
||||
import { ProfileFormFields } from '@/components/settings/profile-form-fields';
|
||||
import {
|
||||
createAgentAction,
|
||||
createAgentProfileAction,
|
||||
@@ -58,6 +48,7 @@ type AgentSetupFormProps = {
|
||||
savedAgent: Agent | null;
|
||||
discoveryAgent: AgentDiscovery | undefined;
|
||||
onToastError: (error: unknown) => void;
|
||||
isCreateMode?: boolean;
|
||||
};
|
||||
|
||||
const createDraftProfile = (
|
||||
@@ -75,7 +66,6 @@ const createDraftProfile = (
|
||||
dangerously_skip_permissions: permissionSettings?.dangerously_skip_permissions?.default ?? false,
|
||||
allow_indexing: permissionSettings?.allow_indexing?.default ?? false,
|
||||
cli_passthrough: false,
|
||||
plan: '',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
isNew: true,
|
||||
@@ -109,6 +99,7 @@ function AgentSetupForm({
|
||||
savedAgent,
|
||||
discoveryAgent,
|
||||
onToastError,
|
||||
isCreateMode = false,
|
||||
}: AgentSetupFormProps) {
|
||||
const router = useRouter();
|
||||
const settingsAgents = useAppStore((state) => state.settingsAgents.items);
|
||||
@@ -130,13 +121,17 @@ function AgentSetupForm({
|
||||
}, [availableAgents, draftAgent.name]);
|
||||
|
||||
const currentAgentModelConfig = useMemo(() => {
|
||||
return currentAvailableAgent?.model_config ?? { default_model: '', available_models: [] };
|
||||
return currentAvailableAgent?.model_config ?? { default_model: '', available_models: [], supports_dynamic_models: false };
|
||||
}, [currentAvailableAgent]);
|
||||
|
||||
const permissionSettings = useMemo(() => {
|
||||
return currentAvailableAgent?.permission_settings ?? {};
|
||||
}, [currentAvailableAgent]);
|
||||
|
||||
const passthroughConfig = useMemo(() => {
|
||||
return currentAvailableAgent?.passthrough_config ?? null;
|
||||
}, [currentAvailableAgent]);
|
||||
|
||||
const isProfileDirty = (draft: DraftProfile, saved?: AgentProfile) => {
|
||||
if (!saved) return true;
|
||||
return (
|
||||
@@ -145,7 +140,7 @@ function AgentSetupForm({
|
||||
draft.auto_approve !== saved.auto_approve ||
|
||||
draft.dangerously_skip_permissions !== saved.dangerously_skip_permissions ||
|
||||
draft.allow_indexing !== saved.allow_indexing ||
|
||||
draft.plan !== saved.plan
|
||||
draft.cli_passthrough !== saved.cli_passthrough
|
||||
);
|
||||
};
|
||||
|
||||
@@ -285,7 +280,7 @@ function AgentSetupForm({
|
||||
auto_approve: profile.auto_approve,
|
||||
dangerously_skip_permissions: profile.dangerously_skip_permissions,
|
||||
allow_indexing: profile.allow_indexing ?? false,
|
||||
plan: profile.plan,
|
||||
cli_passthrough: profile.cli_passthrough ?? false,
|
||||
})),
|
||||
});
|
||||
if (created.profiles.length === draftAgent.profiles.length) {
|
||||
@@ -353,7 +348,8 @@ function AgentSetupForm({
|
||||
}
|
||||
|
||||
const savedProfilesById = new Map(savedAgent.profiles.map((profile) => [profile.id, profile]));
|
||||
const nextProfiles: AgentProfile[] = [];
|
||||
// In create mode, preserve existing profiles; otherwise start fresh
|
||||
const nextProfiles: AgentProfile[] = isCreateMode ? [...savedAgent.profiles] : [];
|
||||
for (const profile of draftAgent.profiles) {
|
||||
const savedProfile = savedProfilesById.get(profile.id);
|
||||
if (!savedProfile) {
|
||||
@@ -364,7 +360,6 @@ function AgentSetupForm({
|
||||
dangerously_skip_permissions: profile.dangerously_skip_permissions,
|
||||
allow_indexing: profile.allow_indexing ?? false,
|
||||
cli_passthrough: profile.cli_passthrough ?? false,
|
||||
plan: profile.plan,
|
||||
});
|
||||
if (profile.mcp_config && profile.mcp_config.dirty && profile.mcp_config.servers.trim()) {
|
||||
try {
|
||||
@@ -387,17 +382,19 @@ function AgentSetupForm({
|
||||
auto_approve: profile.auto_approve,
|
||||
dangerously_skip_permissions: profile.dangerously_skip_permissions,
|
||||
allow_indexing: profile.allow_indexing ?? false,
|
||||
plan: profile.plan,
|
||||
});
|
||||
nextProfiles.push(updatedProfile);
|
||||
continue;
|
||||
}
|
||||
nextProfiles.push(savedProfile);
|
||||
}
|
||||
for (const savedProfile of savedAgent.profiles) {
|
||||
const stillExists = draftAgent.profiles.some((profile) => profile.id === savedProfile.id);
|
||||
if (!stillExists) {
|
||||
await deleteAgentProfileAction(savedProfile.id);
|
||||
// In create mode, don't delete existing profiles - we're only adding new ones
|
||||
if (!isCreateMode) {
|
||||
for (const savedProfile of savedAgent.profiles) {
|
||||
const stillExists = draftAgent.profiles.some((profile) => profile.id === savedProfile.id);
|
||||
if (!stillExists) {
|
||||
await deleteAgentProfileAction(savedProfile.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,6 +406,10 @@ function AgentSetupForm({
|
||||
};
|
||||
upsertAgent(nextAgent);
|
||||
setDraftAgent(ensureProfiles(cloneAgent(nextAgent), resolveDisplayName(nextAgent.name), currentAgentModelConfig.default_model, permissionSettings));
|
||||
// In create mode, redirect to manage mode after successful save
|
||||
if (isCreateMode) {
|
||||
router.replace(`/settings/agents/${encodeURIComponent(savedAgent.name)}`);
|
||||
}
|
||||
}
|
||||
setSaveStatus('success');
|
||||
} catch (error) {
|
||||
@@ -430,7 +431,7 @@ function AgentSetupForm({
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Configure profiles and defaults for this agent.
|
||||
{isCreateMode ? 'Create a new profile for this agent.' : 'Configure profiles and defaults for this agent.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -440,7 +441,11 @@ function AgentSetupForm({
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<CardTitle>{draftAgent.profiles[0]?.agent_display_name ?? draftAgent.name} Profiles</CardTitle>
|
||||
<CardTitle>
|
||||
{isCreateMode
|
||||
? `Create ${draftAgent.profiles[0]?.agent_display_name ?? draftAgent.name} Profile`
|
||||
: `${draftAgent.profiles[0]?.agent_display_name ?? draftAgent.name} Profiles`}
|
||||
</CardTitle>
|
||||
{isAgentDirty && <UnsavedChangesBadge />}
|
||||
</div>
|
||||
<Button size="sm" variant="outline" onClick={handleAddProfile} className="cursor-pointer">
|
||||
@@ -458,119 +463,23 @@ function AgentSetupForm({
|
||||
className={isNew ? 'border-amber-400/70 shadow-sm' : 'border-muted'}
|
||||
>
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 space-y-2">
|
||||
<Label>Profile name</Label>
|
||||
<Input
|
||||
value={profile.name}
|
||||
onChange={(event) =>
|
||||
handleProfileChange(profile.id, { name: event.target.value })
|
||||
}
|
||||
placeholder="Default profile"
|
||||
/>
|
||||
</div>
|
||||
{draftAgent.profiles.length > 1 && (
|
||||
<Button size="sm" variant="ghost" onClick={() => handleRemoveProfile(profile.id)}>
|
||||
Remove
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Model</Label>
|
||||
{currentAgentModelConfig.available_models.length > 0 ? (
|
||||
<Select
|
||||
value={profile.model || currentAgentModelConfig.default_model}
|
||||
onValueChange={(value) => handleProfileChange(profile.id, { model: value })}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{currentAgentModelConfig.available_models.map((model: { id: string; name: string; is_default?: boolean }) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
{model.name}
|
||||
{model.is_default && (
|
||||
<span className="ml-2 text-muted-foreground">(default)</span>
|
||||
)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
value={profile.model}
|
||||
onChange={(event) =>
|
||||
handleProfileChange(profile.id, { model: event.target.value })
|
||||
}
|
||||
placeholder="model-id"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Append Prompt</Label>
|
||||
<Textarea
|
||||
value={profile.plan}
|
||||
onChange={(event) =>
|
||||
handleProfileChange(profile.id, { plan: event.target.value })
|
||||
}
|
||||
placeholder="Extra text appended to the agent prompt"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{permissionSettings.auto_approve?.supported && (
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{permissionSettings.auto_approve.label}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{permissionSettings.auto_approve.description}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={profile.auto_approve}
|
||||
onCheckedChange={(checked) =>
|
||||
handleProfileChange(profile.id, { auto_approve: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{permissionSettings.dangerously_skip_permissions?.supported && (
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{permissionSettings.dangerously_skip_permissions.label}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{permissionSettings.dangerously_skip_permissions.description}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={profile.dangerously_skip_permissions}
|
||||
onCheckedChange={(checked) =>
|
||||
handleProfileChange(profile.id, { dangerously_skip_permissions: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{permissionSettings.allow_indexing?.supported && (
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{permissionSettings.allow_indexing.label}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{permissionSettings.allow_indexing.description}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={profile.allow_indexing ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleProfileChange(profile.id, { allow_indexing: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ProfileFormFields
|
||||
profile={{
|
||||
name: profile.name,
|
||||
model: profile.model,
|
||||
auto_approve: profile.auto_approve,
|
||||
dangerously_skip_permissions: profile.dangerously_skip_permissions,
|
||||
allow_indexing: profile.allow_indexing,
|
||||
cli_passthrough: profile.cli_passthrough,
|
||||
}}
|
||||
onChange={(patch) => handleProfileChange(profile.id, patch)}
|
||||
modelConfig={currentAgentModelConfig}
|
||||
permissionSettings={permissionSettings}
|
||||
passthroughConfig={passthroughConfig}
|
||||
agentName={draftAgent.name}
|
||||
onRemove={() => handleRemoveProfile(profile.id)}
|
||||
canRemove={draftAgent.profiles.length > 1}
|
||||
/>
|
||||
|
||||
<ProfileMcpConfigCard
|
||||
profileId={profile.id}
|
||||
@@ -602,6 +511,8 @@ function AgentSetupForm({
|
||||
export default function AgentSetupPage() {
|
||||
const { toast } = useToast();
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const isCreateMode = searchParams.get('mode') === 'create';
|
||||
const agentKey = Array.isArray(params.agentId) ? params.agentId[0] : params.agentId;
|
||||
const decodedKey = decodeURIComponent(agentKey ?? '');
|
||||
const discoveryAgents = useAppStore((state) => state.agentDiscovery.items);
|
||||
@@ -627,6 +538,17 @@ export default function AgentSetupPage() {
|
||||
const resolvePermissionSettings = (name: string) =>
|
||||
resolveAvailableAgent(name)?.permission_settings;
|
||||
if (savedAgent) {
|
||||
// In create mode, start with a blank profile instead of loading existing profiles
|
||||
if (isCreateMode) {
|
||||
const draft: DraftAgent = {
|
||||
...savedAgent,
|
||||
workspace_id: savedAgent.workspace_id ?? null,
|
||||
mcp_config_path: savedAgent.mcp_config_path ?? '',
|
||||
profiles: [],
|
||||
isNew: false,
|
||||
};
|
||||
return ensureProfiles(draft, resolveDisplayName(savedAgent.name), resolveDefaultModel(savedAgent.name), resolvePermissionSettings(savedAgent.name));
|
||||
}
|
||||
return ensureProfiles(cloneAgent(savedAgent), resolveDisplayName(savedAgent.name), resolveDefaultModel(savedAgent.name), resolvePermissionSettings(savedAgent.name));
|
||||
}
|
||||
if (discoveryAgent) {
|
||||
@@ -644,7 +566,7 @@ export default function AgentSetupPage() {
|
||||
return ensureProfiles(draft, resolveDisplayName(draft.name), resolveDefaultModel(draft.name), resolvePermissionSettings(draft.name));
|
||||
}
|
||||
return null;
|
||||
}, [decodedKey, discoveryAgent, savedAgent, availableAgents]);
|
||||
}, [decodedKey, discoveryAgent, savedAgent, availableAgents, isCreateMode]);
|
||||
|
||||
if (!initialAgent && discoveryAgents.length > 0) {
|
||||
return (
|
||||
@@ -673,11 +595,12 @@ export default function AgentSetupPage() {
|
||||
|
||||
return (
|
||||
<AgentSetupForm
|
||||
key={initialAgent.id}
|
||||
key={isCreateMode ? `create-${initialAgent.id}` : initialAgent.id}
|
||||
initialAgent={initialAgent}
|
||||
savedAgent={savedAgent}
|
||||
discoveryAgent={discoveryAgent}
|
||||
onToastError={handleToastError}
|
||||
isCreateMode={isCreateMode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function AgentsSettingsPage() {
|
||||
)}
|
||||
</div>
|
||||
<Button size="sm" className="cursor-pointer" asChild>
|
||||
<Link href={`/settings/agents/${encodeURIComponent(agent.name)}`}>
|
||||
<Link href={hasAgentRecord ? `/settings/agents/${encodeURIComponent(agent.name)}?mode=create` : `/settings/agents/${encodeURIComponent(agent.name)}`}>
|
||||
<IconSettings className="h-4 w-4 mr-2" />
|
||||
{hasAgentRecord ? 'Create new profile' : 'Setup Profile'}
|
||||
</Link>
|
||||
|
||||
@@ -6,16 +6,11 @@ import { useParams } from 'next/navigation';
|
||||
import { IconTrash } from '@tabler/icons-react';
|
||||
import { Badge } from '@kandev/ui/badge';
|
||||
import { Button } from '@kandev/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@kandev/ui/tooltip';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kandev/ui/card';
|
||||
import { Label } from '@kandev/ui/label';
|
||||
import { Separator } from '@kandev/ui/separator';
|
||||
import { Switch } from '@kandev/ui/switch';
|
||||
import { Textarea } from '@kandev/ui/textarea';
|
||||
import { Input } from '@kandev/ui/input';
|
||||
import { useToast } from '@/components/toast-provider';
|
||||
import { UnsavedChangesBadge, UnsavedSaveButton } from '@/components/settings/unsaved-indicator';
|
||||
import { ModelCombobox } from '@/components/settings/model-combobox';
|
||||
import { ProfileFormFields } from '@/components/settings/profile-form-fields';
|
||||
import { deleteAgentProfileAction, updateAgentProfileAction } from '@/app/actions/agents';
|
||||
import type { Agent, AgentProfile, ModelConfig, PermissionSetting, PassthroughConfig } from '@/lib/types/http';
|
||||
import { useAppStore } from '@/components/state-provider';
|
||||
@@ -61,8 +56,7 @@ function ProfileEditor({ agent, profile, modelConfig, permissionSettings, passth
|
||||
draft.auto_approve !== savedProfile.auto_approve ||
|
||||
draft.dangerously_skip_permissions !== savedProfile.dangerously_skip_permissions ||
|
||||
draft.allow_indexing !== savedProfile.allow_indexing ||
|
||||
draft.cli_passthrough !== savedProfile.cli_passthrough ||
|
||||
draft.plan !== savedProfile.plan
|
||||
draft.cli_passthrough !== savedProfile.cli_passthrough
|
||||
);
|
||||
}, [draft, savedProfile]);
|
||||
|
||||
@@ -92,7 +86,6 @@ function ProfileEditor({ agent, profile, modelConfig, permissionSettings, passth
|
||||
dangerously_skip_permissions: draft.dangerously_skip_permissions,
|
||||
allow_indexing: draft.allow_indexing,
|
||||
cli_passthrough: draft.cli_passthrough,
|
||||
plan: draft.plan,
|
||||
});
|
||||
setSavedProfile(updated);
|
||||
setDraft(updated);
|
||||
@@ -170,118 +163,21 @@ function ProfileEditor({ agent, profile, modelConfig, permissionSettings, passth
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Profile name</Label>
|
||||
<Input
|
||||
value={draft.name}
|
||||
onChange={(event) => setDraft({ ...draft, name: event.target.value })}
|
||||
placeholder="Default profile"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Model</Label>
|
||||
<ModelCombobox
|
||||
value={draft.model || modelConfig.default_model}
|
||||
onChange={(value) => setDraft({ ...draft, model: value })}
|
||||
models={modelConfig.available_models}
|
||||
defaultModel={modelConfig.default_model}
|
||||
placeholder="Select or enter model..."
|
||||
agentName={agent.name}
|
||||
supportsDynamicModels={modelConfig.supports_dynamic_models}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Append Prompt</Label>
|
||||
<Textarea
|
||||
value={draft.plan}
|
||||
onChange={(event) => setDraft({ ...draft, plan: event.target.value })}
|
||||
placeholder="Extra text appended to the agent prompt"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{permissionSettings.auto_approve?.supported && (
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{permissionSettings.auto_approve.label}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{permissionSettings.auto_approve.description}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={draft.auto_approve}
|
||||
onCheckedChange={(checked) =>
|
||||
setDraft({ ...draft, auto_approve: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{permissionSettings.dangerously_skip_permissions?.supported && (
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{permissionSettings.dangerously_skip_permissions.label}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{permissionSettings.dangerously_skip_permissions.description}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={draft.dangerously_skip_permissions}
|
||||
onCheckedChange={(checked) =>
|
||||
setDraft({ ...draft, dangerously_skip_permissions: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{permissionSettings.allow_indexing?.supported && (
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{permissionSettings.allow_indexing.label}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{permissionSettings.allow_indexing.description}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={draft.allow_indexing}
|
||||
onCheckedChange={(checked) =>
|
||||
setDraft({ ...draft, allow_indexing: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{passthroughConfig?.supported && (
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>{passthroughConfig.label}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0 cursor-help">
|
||||
Experimental
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Currently does not support notifications and session resume</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{passthroughConfig.description}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={draft.cli_passthrough}
|
||||
onCheckedChange={(checked) =>
|
||||
setDraft({ ...draft, cli_passthrough: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ProfileFormFields
|
||||
profile={{
|
||||
name: draft.name,
|
||||
model: draft.model,
|
||||
auto_approve: draft.auto_approve,
|
||||
dangerously_skip_permissions: draft.dangerously_skip_permissions,
|
||||
allow_indexing: draft.allow_indexing,
|
||||
cli_passthrough: draft.cli_passthrough,
|
||||
}}
|
||||
onChange={(patch) => setDraft({ ...draft, ...patch })}
|
||||
modelConfig={modelConfig}
|
||||
permissionSettings={permissionSettings}
|
||||
passthroughConfig={passthroughConfig}
|
||||
agentName={agent.name}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
'use client';
|
||||
|
||||
import { Badge } from '@kandev/ui/badge';
|
||||
import { Button } from '@kandev/ui/button';
|
||||
import { Input } from '@kandev/ui/input';
|
||||
import { Label } from '@kandev/ui/label';
|
||||
import { Switch } from '@kandev/ui/switch';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@kandev/ui/tooltip';
|
||||
import { ModelCombobox } from '@/components/settings/model-combobox';
|
||||
import type { ModelConfig, PermissionSetting, PassthroughConfig } from '@/lib/types/http';
|
||||
|
||||
export type ProfileFormData = {
|
||||
name: string;
|
||||
model: string;
|
||||
auto_approve: boolean;
|
||||
dangerously_skip_permissions: boolean;
|
||||
allow_indexing?: boolean;
|
||||
cli_passthrough: boolean;
|
||||
};
|
||||
|
||||
export type ProfileFormFieldsProps = {
|
||||
profile: ProfileFormData;
|
||||
onChange: (patch: Partial<ProfileFormData>) => void;
|
||||
modelConfig: ModelConfig;
|
||||
permissionSettings: Record<string, PermissionSetting>;
|
||||
passthroughConfig: PassthroughConfig | null;
|
||||
agentName: string;
|
||||
onRemove?: () => void;
|
||||
canRemove?: boolean;
|
||||
};
|
||||
|
||||
export function ProfileFormFields({
|
||||
profile,
|
||||
onChange,
|
||||
modelConfig,
|
||||
permissionSettings,
|
||||
passthroughConfig,
|
||||
agentName,
|
||||
onRemove,
|
||||
canRemove = false,
|
||||
}: ProfileFormFieldsProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 space-y-2">
|
||||
<Label>Profile name</Label>
|
||||
<Input
|
||||
value={profile.name}
|
||||
onChange={(event) => onChange({ name: event.target.value })}
|
||||
placeholder="Default profile"
|
||||
/>
|
||||
</div>
|
||||
{canRemove && onRemove && (
|
||||
<Button size="sm" variant="ghost" onClick={onRemove}>
|
||||
Remove
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Model</Label>
|
||||
<ModelCombobox
|
||||
value={profile.model || modelConfig.default_model}
|
||||
onChange={(value) => onChange({ model: value })}
|
||||
models={modelConfig.available_models}
|
||||
defaultModel={modelConfig.default_model}
|
||||
placeholder="Select or enter model..."
|
||||
agentName={agentName}
|
||||
supportsDynamicModels={modelConfig.supports_dynamic_models}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{permissionSettings.auto_approve?.supported && (
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{permissionSettings.auto_approve.label}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{permissionSettings.auto_approve.description}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={profile.auto_approve}
|
||||
onCheckedChange={(checked) => onChange({ auto_approve: checked })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{permissionSettings.dangerously_skip_permissions?.supported && (
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{permissionSettings.dangerously_skip_permissions.label}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{permissionSettings.dangerously_skip_permissions.description}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={profile.dangerously_skip_permissions}
|
||||
onCheckedChange={(checked) => onChange({ dangerously_skip_permissions: checked })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{permissionSettings.allow_indexing?.supported && (
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{permissionSettings.allow_indexing.label}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{permissionSettings.allow_indexing.description}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={profile.allow_indexing ?? false}
|
||||
onCheckedChange={(checked) => onChange({ allow_indexing: checked })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{passthroughConfig?.supported && (
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>{passthroughConfig.label}</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{passthroughConfig.description}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={profile.cli_passthrough}
|
||||
onCheckedChange={(checked) => onChange({ cli_passthrough: checked })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -518,7 +518,6 @@ export type AgentProfile = {
|
||||
dangerously_skip_permissions: boolean;
|
||||
allow_indexing: boolean;
|
||||
cli_passthrough: boolean;
|
||||
plan: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user