Files
kandev/apps/web/components/settings/agent-profile-page.tsx
T
2026-02-03 22:07:34 +00:00

265 lines
9.3 KiB
TypeScript

'use client';
import { useMemo, useState } from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { IconTrash } from '@tabler/icons-react';
import { Badge } from '@kandev/ui/badge';
import { Button } from '@kandev/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kandev/ui/card';
import { Separator } from '@kandev/ui/separator';
import { useToast } from '@/components/toast-provider';
import { UnsavedChangesBadge, UnsavedSaveButton } from '@/components/settings/unsaved-indicator';
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';
import { ProfileMcpConfigCard } from '@/app/settings/agents/[agentId]/profile-mcp-config-card';
import { CommandPreviewCard } from '@/app/settings/agents/[agentId]/profiles/[profileId]/command-preview-card';
import type { AgentProfileMcpConfig } from '@/lib/types/http';
import { useAgentProfileSettings } from '@/app/settings/agents/[agentId]/profiles/[profileId]/use-agent-profile-settings';
type ProfileEditorProps = {
agent: Agent;
profile: AgentProfile;
modelConfig: ModelConfig;
permissionSettings: Record<string, PermissionSetting>;
passthroughConfig: PassthroughConfig | null;
initialMcpConfig?: AgentProfileMcpConfig | null;
};
function ProfileEditor({ agent, profile, modelConfig, permissionSettings, passthroughConfig, initialMcpConfig }: ProfileEditorProps) {
const { toast } = useToast();
const settingsAgents = useAppStore((state) => state.settingsAgents.items);
const setSettingsAgents = useAppStore((state) => state.setSettingsAgents);
const setAgentProfiles = useAppStore((state) => state.setAgentProfiles);
const [draft, setDraft] = useState<AgentProfile>({ ...profile });
const [savedProfile, setSavedProfile] = useState<AgentProfile>(profile);
const [saveStatus, setSaveStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const syncAgentsToStore = (nextAgents: Agent[]) => {
setSettingsAgents(nextAgents);
setAgentProfiles(
nextAgents.flatMap((agentItem) =>
agentItem.profiles.map((agentProfile) => ({
id: agentProfile.id,
label: `${agentProfile.agent_display_name} • ${agentProfile.name}`,
agent_id: agentItem.id,
}))
)
);
};
const isDirty = useMemo(() => {
return (
draft.name !== savedProfile.name ||
draft.model !== savedProfile.model ||
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, savedProfile]);
const handleSave = async () => {
if (!draft.name.trim()) {
toast({
title: 'Profile name is required',
description: 'Please enter a profile name before saving.',
variant: 'error',
});
return;
}
if (!draft.model.trim()) {
toast({
title: 'Model is required',
description: 'Please select a model before saving.',
variant: 'error',
});
return;
}
setSaveStatus('loading');
try {
const updated = await updateAgentProfileAction(draft.id, {
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,
});
setSavedProfile(updated);
setDraft(updated);
const nextAgents = settingsAgents.map((agentItem: Agent) =>
agentItem.id === agent.id
? {
...agentItem,
profiles: agentItem.profiles.map((profileItem: AgentProfile) =>
profileItem.id === updated.id ? updated : profileItem
),
}
: agentItem
);
syncAgentsToStore(nextAgents);
setSaveStatus('success');
} catch (error) {
setSaveStatus('error');
toast({
title: 'Failed to save profile',
description: error instanceof Error ? error.message : 'Request failed',
variant: 'error',
});
}
};
const handleDeleteProfile = async () => {
try {
await deleteAgentProfileAction(draft.id);
const nextAgents = settingsAgents.map((agentItem: Agent) =>
agentItem.id === agent.id
? {
...agentItem,
profiles: agentItem.profiles.filter((profileItem: AgentProfile) => profileItem.id !== draft.id),
}
: agentItem
);
syncAgentsToStore(nextAgents);
window.location.assign('/settings/agents');
} catch (error) {
toast({
title: 'Failed to delete profile',
description: error instanceof Error ? error.message : 'Request failed',
variant: 'error',
});
}
};
return (
<div className="space-y-8">
<div className="flex items-start justify-between">
<div>
<h2 className="text-2xl font-bold">{profile.agent_display_name} • {savedProfile.name}</h2>
<p className="text-sm text-muted-foreground mt-1">
{profile.agent_display_name} profile settings
</p>
</div>
<div className="flex items-center gap-3">
{isDirty && <UnsavedChangesBadge />}
<UnsavedSaveButton
isDirty={isDirty}
isLoading={saveStatus === 'loading'}
status={saveStatus}
onClick={handleSave}
/>
</div>
</div>
<Separator />
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<span>Profile settings</span>
{agent.supports_mcp && <Badge variant="secondary">MCP</Badge>}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<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>
<CommandPreviewCard
agentName={agent.name}
model={draft.model}
permissionSettings={{
auto_approve: draft.auto_approve,
dangerously_skip_permissions: draft.dangerously_skip_permissions,
allow_indexing: draft.allow_indexing,
}}
cliPassthrough={draft.cli_passthrough}
/>
<ProfileMcpConfigCard
profileId={profile.id}
supportsMcp={agent.supports_mcp}
initialConfig={initialMcpConfig}
onToastError={(error) =>
toast({
title: 'Failed to save MCP config',
description: error instanceof Error ? error.message : 'Request failed',
variant: 'error',
})
}
/>
<Card className="border-destructive">
<CardHeader>
<CardTitle className="text-destructive">Delete profile</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">Remove this profile</p>
<p className="text-xs text-muted-foreground">This action cannot be undone.</p>
</div>
<Button variant="destructive" onClick={handleDeleteProfile}>
<IconTrash className="h-4 w-4 mr-2" />
Delete
</Button>
</CardContent>
</Card>
</div>
);
}
type AgentProfilePageClientProps = {
initialMcpConfig?: AgentProfileMcpConfig | null;
};
export function AgentProfilePage({ initialMcpConfig }: AgentProfilePageClientProps) {
const params = useParams();
const agentParam = Array.isArray(params.agentId) ? params.agentId[0] : params.agentId;
const profileParam = Array.isArray(params.profileId) ? params.profileId[0] : params.profileId;
const agentKey = decodeURIComponent(agentParam ?? '');
const profileId = profileParam ?? '';
const { agent, profile, modelConfig, permissionSettings, passthroughConfig } = useAgentProfileSettings(agentKey, profileId);
if (!agent || !profile) {
return (
<Card>
<CardContent className="py-12 text-center">
<p className="text-sm text-muted-foreground">Profile not found.</p>
<Button className="mt-4" asChild>
<Link href="/settings/agents">Back to Agents</Link>
</Button>
</CardContent>
</Card>
);
}
return (
<ProfileEditor
key={profile.id}
agent={agent}
profile={profile}
modelConfig={modelConfig}
permissionSettings={permissionSettings}
passthroughConfig={passthroughConfig}
initialMcpConfig={initialMcpConfig}
/>
);
}