diff --git a/desktop/src/features/agents/ui/PersonaDeleteDialog.tsx b/desktop/src/features/agents/ui/PersonaDeleteDialog.tsx index 567bc0788..40145fed9 100644 --- a/desktop/src/features/agents/ui/PersonaDeleteDialog.tsx +++ b/desktop/src/features/agents/ui/PersonaDeleteDialog.tsx @@ -28,11 +28,11 @@ export function PersonaDeleteDialog({ - Delete persona? + Delete agent? {persona - ? `Delete ${persona.displayName}. Existing agents keep their copied settings, but this template will no longer be available for new deployments.` - : "Delete this persona."} + ? `Delete ${persona.displayName} and any managed instance tied to it. This cannot be undone.` + : "Delete this agent."} diff --git a/desktop/src/features/profile/ui/UserProfilePanel.tsx b/desktop/src/features/profile/ui/UserProfilePanel.tsx index 94017e4fb..453693c01 100644 --- a/desktop/src/features/profile/ui/UserProfilePanel.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanel.tsx @@ -31,7 +31,6 @@ import { AddAgentToChannelDialog } from "@/features/agents/ui/AddAgentToChannelD import { useActiveAgentTurnsBridge } from "@/features/agents/activeAgentTurnsStore"; import { resolvePersonaRuntime } from "@/features/agents/lib/resolvePersonaRuntime"; import { - deleteManagedAgentWithRules, isManagedAgentActive, startManagedAgentWithRules, stopManagedAgentWithRules, @@ -63,12 +62,12 @@ import { ModelFocusedView, ProfileSummaryView, } from "@/features/profile/ui/UserProfilePanelSections"; +import { useProfileAgentDeletion } from "@/features/profile/ui/UserProfilePanelDeletion"; import { useProfileFieldBuckets } from "@/features/profile/ui/UserProfilePanelFields"; import { UserProfilePersonaDialogs } from "@/features/profile/ui/UserProfilePersonaDialogs"; import { buildPersonaDraftProfile, deriveProfileChannels, - getRelayAgentChannelIds, PROFILE_PANEL_VIEW_TITLES, type ProfilePanelView, resolveAgentInstruction, @@ -82,7 +81,6 @@ import { useAgentSession } from "@/shared/context/AgentSessionContext"; import { useEscapeKey } from "@/shared/hooks/useEscapeKey"; import { useIsThreadPanelOverlay } from "@/shared/hooks/use-mobile"; import { THREAD_PANEL_MIN_WIDTH_PX } from "@/shared/hooks/useThreadPanelWidth"; -import { removeChannelMember } from "@/shared/api/tauri"; import { AuxiliaryPanelHeader, AuxiliaryPanelHeaderGroup, @@ -327,21 +325,18 @@ export function UserProfilePanel({ setEditAgentOpen(true); }, [resolvedPersona]); - const removeAgentFromAllChannels = React.useCallback( - async (agentPubkey: string) => { - const channelIds = getRelayAgentChannelIds( - relayAgentsQuery.data, - agentPubkey, - ); - if (channelIds.length === 0) return; - await Promise.allSettled( - channelIds.map((channelId) => - removeChannelMember(channelId, agentPubkey), - ), - ); - }, - [relayAgentsQuery.data], - ); + const { + deleteManagedAgentRecord, + deleteManagedAgentsForPersona, + removeAgentFromAllChannels, + } = useProfileAgentDeletion({ + channels: channelsQuery.data, + deleteManagedAgent: deleteAgentMutation.mutateAsync, + managedAgent, + managedAgents: managedAgentsQuery.data, + presenceLookup: presenceQuery.data, + relayAgents: relayAgentsQuery.data, + }); const handleAgentPrimaryAction = React.useCallback(async () => { if (!managedAgent) return; @@ -480,16 +475,9 @@ export function UserProfilePanel({ if (!managedAgent) return; try { - const result = await deleteManagedAgentWithRules({ - agent: managedAgent, - channels: channelsQuery.data ?? [], - deleteManagedAgent: deleteAgentMutation.mutateAsync, - presenceLookup: presenceQuery.data, - relayAgents: relayAgentsQuery.data ?? [], - }); + const result = await deleteManagedAgentRecord(managedAgent); if (result.cancelled) return; - await removeAgentFromAllChannels(managedAgent.pubkey); toast.success(`Deleted ${managedAgent.name}.`); onClose(); } catch (error) { @@ -497,15 +485,7 @@ export function UserProfilePanel({ error instanceof Error ? error.message : "Failed to delete agent.", ); } - }, [ - channelsQuery.data, - deleteAgentMutation.mutateAsync, - managedAgent, - onClose, - presenceQuery.data, - relayAgentsQuery.data, - removeAgentFromAllChannels, - ]); + }, [deleteManagedAgentRecord, managedAgent, onClose]); const handleSubmitPersona = React.useCallback( async (input: CreatePersonaInput | UpdatePersonaInput) => { @@ -563,6 +543,10 @@ export function UserProfilePanel({ if (resolvedPersona.isBuiltIn) { try { + const deletedInstances = + await deleteManagedAgentsForPersona(resolvedPersona); + if (!deletedInstances) return; + await setPersonaActiveMutation.mutateAsync({ id: resolvedPersona.id, active: false, @@ -583,11 +567,20 @@ export function UserProfilePanel({ } setPersonaToDelete(resolvedPersona); - }, [onClose, resolvedPersona, setPersonaActiveMutation.mutateAsync]); + }, [ + deleteManagedAgentsForPersona, + onClose, + resolvedPersona, + setPersonaActiveMutation.mutateAsync, + ]); const handleConfirmDeletePersona = React.useCallback( async (personaToConfirm: AgentPersona) => { try { + const deletedInstances = + await deleteManagedAgentsForPersona(personaToConfirm); + if (!deletedInstances) return; + await deletePersonaMutation.mutateAsync(personaToConfirm.id); toast.success(`Deleted ${personaToConfirm.displayName}.`); setPersonaToDelete(null); @@ -598,7 +591,7 @@ export function UserProfilePanel({ ); } }, - [deletePersonaMutation.mutateAsync, onClose], + [deleteManagedAgentsForPersona, deletePersonaMutation.mutateAsync, onClose], ); const handleAddedToChannel = React.useCallback( diff --git a/desktop/src/features/profile/ui/UserProfilePanelDeletion.ts b/desktop/src/features/profile/ui/UserProfilePanelDeletion.ts new file mode 100644 index 000000000..3367d84a2 --- /dev/null +++ b/desktop/src/features/profile/ui/UserProfilePanelDeletion.ts @@ -0,0 +1,147 @@ +import * as React from "react"; + +import { + deleteManagedAgentWithRules, + type ManagedAgentActionResult, +} from "@/features/agents/lib/managedAgentControlActions"; +import { removeChannelMember } from "@/shared/api/tauri"; +import type { + AgentPersona, + Channel, + ManagedAgent, + PresenceLookup, + RelayAgent, +} from "@/shared/api/types"; +import { getRelayAgentChannelIds } from "@/features/profile/ui/UserProfilePanelUtils"; + +type DeleteManagedAgentRulesContext = Omit< + Parameters[0], + "agent" +>; + +type DeleteProfileManagedAgentContext = DeleteManagedAgentRulesContext & { + removeAgentFromAllChannels: (pubkey: string) => Promise; +}; + +type DeleteProfileManagedAgentsForPersonaContext = + DeleteProfileManagedAgentContext & { + managedAgents: readonly ManagedAgent[]; + selectedAgent?: ManagedAgent; + }; + +type UseProfileAgentDeletionInput = { + channels?: readonly Channel[]; + deleteManagedAgent: DeleteManagedAgentRulesContext["deleteManagedAgent"]; + managedAgent?: ManagedAgent; + managedAgents?: readonly ManagedAgent[]; + presenceLookup?: PresenceLookup | null; + relayAgents?: readonly RelayAgent[]; +}; + +export function useProfileAgentDeletion({ + channels, + deleteManagedAgent, + managedAgent, + managedAgents, + presenceLookup, + relayAgents, +}: UseProfileAgentDeletionInput) { + const removeAgentFromAllChannels = React.useCallback( + async (agentPubkey: string) => { + const channelIds = getRelayAgentChannelIds(relayAgents, agentPubkey); + if (channelIds.length === 0) return; + await Promise.allSettled( + channelIds.map((channelId) => + removeChannelMember(channelId, agentPubkey), + ), + ); + }, + [relayAgents], + ); + + const deleteManagedAgentRecord = React.useCallback( + (agentToDelete: ManagedAgent) => + deleteProfileManagedAgent(agentToDelete, { + channels: channels ?? [], + deleteManagedAgent, + presenceLookup, + relayAgents: relayAgents ?? [], + removeAgentFromAllChannels, + }), + [ + channels, + deleteManagedAgent, + presenceLookup, + relayAgents, + removeAgentFromAllChannels, + ], + ); + + const deleteManagedAgentsForPersona = React.useCallback( + (persona: AgentPersona) => + deleteProfileManagedAgentsForPersona(persona, { + channels: channels ?? [], + deleteManagedAgent, + managedAgents: managedAgents ?? [], + presenceLookup, + relayAgents: relayAgents ?? [], + removeAgentFromAllChannels, + selectedAgent: managedAgent, + }), + [ + channels, + deleteManagedAgent, + managedAgent, + managedAgents, + presenceLookup, + relayAgents, + removeAgentFromAllChannels, + ], + ); + + return { + deleteManagedAgentRecord, + deleteManagedAgentsForPersona, + removeAgentFromAllChannels, + }; +} + +export async function deleteProfileManagedAgent( + agent: ManagedAgent, + context: DeleteProfileManagedAgentContext, +): Promise { + const { removeAgentFromAllChannels, ...deleteContext } = context; + const result = await deleteManagedAgentWithRules({ + agent, + ...deleteContext, + }); + if (result.cancelled) return result; + + await removeAgentFromAllChannels(agent.pubkey); + return result; +} + +export async function deleteProfileManagedAgentsForPersona( + persona: AgentPersona, + context: DeleteProfileManagedAgentsForPersonaContext, +): Promise { + const { managedAgents, selectedAgent, ...deleteContext } = context; + const agentsByPubkey = new Map(); + + for (const agent of managedAgents) { + if (agent.personaId === persona.id) { + agentsByPubkey.set(agent.pubkey, agent); + } + } + + if (selectedAgent?.personaId === persona.id) { + agentsByPubkey.set(selectedAgent.pubkey, selectedAgent); + } + + for (const agent of agentsByPubkey.values()) { + const result = await deleteProfileManagedAgent(agent, deleteContext); + if (result.cancelled) return result; + } + + return {}; +} diff --git a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx index b78297795..d40381dd6 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx @@ -321,7 +321,7 @@ export function ProfileSummaryView({