fix: agent profile creation (#170)

This commit is contained in:
Carlos Florêncio
2026-02-03 22:07:34 +00:00
committed by GitHub
parent 7acdd9619f
commit 0f79342fe8
17 changed files with 227 additions and 301 deletions
-1
View File
@@ -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 {
+2 -3
View File
@@ -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',
+63 -140
View File
@@ -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}
/>
);
}
+1 -1
View File
@@ -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>
);
}
-1
View File
@@ -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;
};