import * as React from "react"; import { ArrowLeft, X } from "lucide-react"; import { toast } from "sonner"; import { useAppNavigation } from "@/app/navigation/useAppNavigation"; import { useAgentMemoryQuery, useIsManagedAgent, } from "@/features/agent-memory/hooks"; import { MemoryRefreshButton } from "@/features/agent-memory/ui/MemorySection"; import { type AttachManagedAgentToChannelResult, useAcpRuntimesQuery, useAvailableAcpRuntimes, useCreateManagedAgentMutation, useCreatePersonaMutation, useDeleteManagedAgentMutation, useDeletePersonaMutation, useExportPersonaJsonMutation, useManagedAgentLogQuery, useRelayAgentsQuery, useManagedAgentsQuery, usePersonasQuery, useSetManagedAgentStartOnAppLaunchMutation, useSetPersonaActiveMutation, useStartManagedAgentMutation, useStopManagedAgentMutation, useUpdatePersonaMutation, } from "@/features/agents/hooks"; import { AddAgentToChannelDialog } from "@/features/agents/ui/AddAgentToChannelDialog"; import { useActiveAgentTurnsBridge } from "@/features/agents/activeAgentTurnsStore"; import { resolvePersonaRuntime } from "@/features/agents/lib/resolvePersonaRuntime"; import { isManagedAgentActive, startManagedAgentWithRules, stopManagedAgentWithRules, } from "@/features/agents/lib/managedAgentControlActions"; import { ManagedAgentLogPanel } from "@/features/agents/ui/ManagedAgentLogPanel"; import { useManagedAgentObserverBridge } from "@/features/agents/observerRelayStore"; import { EditAgentDialog } from "@/features/agents/ui/EditAgentDialog"; import { duplicatePersonaDialogState, editPersonaDialogState, type PersonaDialogState, } from "@/features/agents/ui/personaDialogState"; import { useChannelsQuery } from "@/features/channels/hooks"; import { usePresenceQuery } from "@/features/presence/hooks"; import { useContactListQuery, useFollowMutation, useProfileQuery, useUnfollowMutation, useUserProfileQuery, } from "@/features/profile/hooks"; import { AgentInfoFocusedView, AgentInstructionFocusedView, AgentSettingsFocusedView, ChannelsFocusedView, DiagnosticsFocusedView, MemoryFocusedView, 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, PROFILE_PANEL_VIEW_TITLES, type ProfilePanelView, resolveAgentInstruction, resolveOwnerHandle, resolveProfileDisplayName, type UserProfilePanelProps, useRetainedPersona, } from "@/features/profile/ui/UserProfilePanelUtils"; import { useUserStatusQuery } from "@/features/user-status/hooks"; 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 { AuxiliaryPanelHeader, AuxiliaryPanelHeaderGroup, AuxiliaryPanelTitle, auxiliaryPanelContentPaddingClass, } from "@/shared/layout/AuxiliaryPanelHeader"; import { cn } from "@/shared/lib/cn"; import type { AgentPersona, Channel, CreateManagedAgentInput, CreatePersonaInput, UpdatePersonaInput, } from "@/shared/api/types"; import { Button } from "@/shared/ui/button"; import { OverlayPanelBackdrop, PANEL_BASE_CLASS, PANEL_OVERLAY_CLASS, PANEL_SINGLE_COLUMN_HEADER_LAYER_CLASS, } from "@/shared/ui/OverlayPanelBackdrop"; export type { ProfilePanelView }; export function UserProfilePanel({ canResetWidth, currentPubkey, isSinglePanelView = false, layout = "standalone", onClose, onOpenDm, onResetWidth, onResizeStart, onViewChange, persona, pubkey, splitPaneClamp = false, view: controlledView, widthPx, }: UserProfilePanelProps) { const isOverlay = useIsThreadPanelOverlay(); const isFloatingOverlay = isOverlay && !isSinglePanelView; const isSplitLayout = layout === "split"; useEscapeKey(onClose, isOverlay || isSinglePanelView); const [internalView, setInternalView] = React.useState("summary"); const view = controlledView ?? internalView; const setView = React.useCallback( (nextView: ProfilePanelView, options?: { replace?: boolean }) => { if (onViewChange) { onViewChange(nextView, options); return; } setInternalView(nextView); }, [onViewChange], ); const [editAgentOpen, setEditAgentOpen] = React.useState(false); const [addToChannelOpen, setAddToChannelOpen] = React.useState(false); const [personaDialogState, setPersonaDialogState] = React.useState(null); const [personaToDelete, setPersonaToDelete] = React.useState(null); const personasQuery = usePersonasQuery(); const managedAgentsQuery = useManagedAgentsQuery({ enabled: true }); const managedAgent = React.useMemo(() => { const agents = managedAgentsQuery.data ?? []; if (pubkey) { const pubkeyLower = pubkey.toLowerCase(); return agents.find((agent) => agent.pubkey.toLowerCase() === pubkeyLower); } if (persona) { return agents.find((agent) => agent.personaId === persona.id); } return undefined; }, [managedAgentsQuery.data, persona, pubkey]); const resolvedPersonaFromSource = React.useMemo(() => { const personaId = persona?.id ?? managedAgent?.personaId; if (personaId) { const refreshedPersona = personasQuery.data?.find( (candidate) => candidate.id === personaId, ); if (refreshedPersona) { return refreshedPersona; } } if (persona) { return persona; } if (!managedAgent?.personaId) { return undefined; } return personasQuery.data?.find( (candidate) => candidate.id === managedAgent.personaId, ); }, [managedAgent?.personaId, persona, personasQuery.data]); const profileIdentityKey = pubkey ?? managedAgent?.pubkey ?? `persona:${persona?.id ?? "unknown"}`; const resolvedPersona = useRetainedPersona( resolvedPersonaFromSource, profileIdentityKey, ); const effectivePubkey = pubkey ?? managedAgent?.pubkey ?? null; const pubkeyLower = effectivePubkey?.toLowerCase() ?? ""; const profileQuery = useUserProfileQuery(effectivePubkey ?? undefined); const currentProfileQuery = useProfileQuery(currentPubkey !== undefined); React.useEffect(() => { if (!effectivePubkey) return; void profileQuery.refetch(); }, [effectivePubkey, profileQuery.refetch]); const relayAgentsQuery = useRelayAgentsQuery({ enabled: true }); const availableRuntimesQuery = useAvailableAcpRuntimes(); const acpRuntimesQuery = useAcpRuntimesQuery(); const createAgentMutation = useCreateManagedAgentMutation(); const startAgentMutation = useStartManagedAgentMutation(); const stopAgentMutation = useStopManagedAgentMutation(); const deleteAgentMutation = useDeleteManagedAgentMutation(); const startOnLaunchMutation = useSetManagedAgentStartOnAppLaunchMutation(); const createPersonaMutation = useCreatePersonaMutation(); const updatePersonaMutation = useUpdatePersonaMutation(); const deletePersonaMutation = useDeletePersonaMutation(); const setPersonaActiveMutation = useSetPersonaActiveMutation(); const exportPersonaJsonMutation = useExportPersonaJsonMutation(); const channelsQuery = useChannelsQuery(); const presenceQuery = usePresenceQuery( effectivePubkey ? [effectivePubkey] : [], ); const userStatusQuery = useUserStatusQuery( effectivePubkey ? [effectivePubkey] : [], ); const contactListQuery = useContactListQuery(currentPubkey); const followMutation = useFollowMutation(currentPubkey); const unfollowMutation = useUnfollowMutation(currentPubkey); const { onOpenAgentSession } = useAgentSession(); const { goChannel } = useAppNavigation(); const profile = profileQuery.data ?? (resolvedPersona ? buildPersonaDraftProfile(resolvedPersona) : undefined); const presenceStatus = pubkeyLower ? presenceQuery.data?.[pubkeyLower] : undefined; const userStatus = pubkeyLower ? userStatusQuery.data?.[pubkeyLower] : undefined; const relayAgent = relayAgentsQuery.data?.find( (agent) => agent.pubkey.toLowerCase() === pubkeyLower, ); const managedAgentLogQuery = useManagedAgentLogQuery( view === "logs" && managedAgent?.backend.type === "local" ? managedAgent.pubkey : null, ); const isBot = Boolean(relayAgent || managedAgent || resolvedPersona); const managedAgentOwner = useIsManagedAgent(isBot ? effectivePubkey : null); const isOwner = resolvedPersona ? true : managedAgentOwner; // Populate the active-turns store for this agent so useActiveAgentTurns works // even if the Agents page hasn't been visited yet. const bridgeAgents = React.useMemo( () => managedAgent ? [{ pubkey: managedAgent.pubkey, status: managedAgent.status }] : [], [managedAgent], ); useActiveAgentTurnsBridge(bridgeAgents); useManagedAgentObserverBridge(bridgeAgents); const canEditAgent = isOwner === true && (managedAgent !== undefined || (resolvedPersona !== undefined && !resolvedPersona.isBuiltIn)); const memoryQuery = useAgentMemoryQuery(effectivePubkey, { enabled: isOwner === true && Boolean(effectivePubkey), }); const isSelf = currentPubkey !== undefined && pubkeyLower.length > 0 && pubkeyLower === currentPubkey.toLowerCase(); const canViewActivity = isOwner === true && Boolean(onOpenAgentSession) && Boolean(effectivePubkey); const canOpenAgentLogs = isOwner === true && managedAgent?.backend.type === "local"; const canInstantiateAgent = isOwner === true && resolvedPersona !== undefined && managedAgent === undefined; const isAgentActionPending = createAgentMutation.isPending || startAgentMutation.isPending || stopAgentMutation.isPending || deleteAgentMutation.isPending || startOnLaunchMutation.isPending || createPersonaMutation.isPending || updatePersonaMutation.isPending || deletePersonaMutation.isPending || setPersonaActiveMutation.isPending || exportPersonaJsonMutation.isPending; const isFollowing = !isSelf && pubkeyLower.length > 0 && (contactListQuery.data?.contacts.some( (contact) => contact.pubkey.toLowerCase() === pubkeyLower, ) ?? false); const profileChannels = React.useMemo( () => deriveProfileChannels( pubkeyLower, relayAgent, managedAgent, channelsQuery.data, ), [pubkeyLower, relayAgent, managedAgent, channelsQuery.data], ); const channelIdToName = React.useMemo(() => { const map: Record = {}; for (const channel of channelsQuery.data ?? []) { map[channel.id] = channel.name; } return map; }, [channelsQuery.data]); const targetKey = effectivePubkey ?? `persona:${resolvedPersona?.id ?? "unknown"}`; const prevTargetKeyRef = React.useRef(targetKey); React.useEffect(() => { if (prevTargetKeyRef.current === targetKey) return; prevTargetKeyRef.current = targetKey; setView("summary", { replace: true }); }, [setView, targetKey]); const handleMessage = React.useCallback(() => { if (!effectivePubkey) return; onOpenDm?.([effectivePubkey]); onClose(); }, [effectivePubkey, onClose, onOpenDm]); const handleEditAgent = React.useCallback(() => { if (resolvedPersona && !resolvedPersona.isBuiltIn) { setPersonaDialogState(editPersonaDialogState(resolvedPersona)); return; } setEditAgentOpen(true); }, [resolvedPersona]); const { deleteManagedAgentRecord, deleteManagedAgentsForPersona } = useProfileAgentDeletion({ channels: channelsQuery.data, deleteManagedAgent: deleteAgentMutation.mutateAsync, managedAgent, managedAgents: managedAgentsQuery.data, presenceLookup: presenceQuery.data, relayAgents: relayAgentsQuery.data, }); const createManagedAgentForPersona = React.useCallback( async (personaToStart: AgentPersona) => { const runtimes = availableRuntimesQuery.data ?? []; const defaultRuntime = runtimes[0] ?? null; const { runtime, warnings } = resolvePersonaRuntime( personaToStart.runtime, runtimes, defaultRuntime, ); for (const warning of warnings) { toast.warning(warning); } if (!runtime) { throw new Error("No available runtime found for this agent."); } const input: CreateManagedAgentInput = { name: personaToStart.displayName, acpCommand: "buzz-acp", agentCommand: runtime.command, agentArgs: runtime.defaultArgs, mcpCommand: runtime.mcpCommand ?? "", personaId: personaToStart.id, systemPrompt: personaToStart.systemPrompt, avatarUrl: personaToStart.avatarUrl ?? undefined, model: personaToStart.model ?? undefined, envVars: personaToStart.envVars, spawnAfterCreate: true, startOnAppLaunch: true, backend: { type: "local" }, }; const created = await createAgentMutation.mutateAsync(input); void managedAgentsQuery.refetch(); void relayAgentsQuery.refetch(); return created; }, [ availableRuntimesQuery.data, createAgentMutation.mutateAsync, managedAgentsQuery.refetch, relayAgentsQuery.refetch, ], ); const handleAgentPrimaryAction = React.useCallback(async () => { if (!managedAgent) return; try { if (isManagedAgentActive(managedAgent)) { const result = await stopManagedAgentWithRules({ agent: managedAgent, channels: channelsQuery.data ?? [], relayAgents: relayAgentsQuery.data ?? [], stopManagedAgent: stopAgentMutation.mutateAsync, }); toast.success(result.noticeMessage ?? `Stopped ${managedAgent.name}.`); return; } await startManagedAgentWithRules({ agent: managedAgent, startManagedAgent: startAgentMutation.mutateAsync, }); toast.success( managedAgent.backend.type === "provider" ? `Deploying ${managedAgent.name}.` : `Started ${managedAgent.name}.`, ); } catch (error) { toast.error( error instanceof Error ? error.message : "Agent action failed.", ); } }, [ channelsQuery.data, managedAgent, relayAgentsQuery.data, startAgentMutation.mutateAsync, stopAgentMutation.mutateAsync, ]); const handleInstantiateAgent = React.useCallback(async () => { if (!resolvedPersona) return; try { const created = await createManagedAgentForPersona(resolvedPersona); if (created.spawnError) { toast.error(created.spawnError); } else { toast.success(`Started ${created.agent.name}.`); } if (created.profileSyncError) { toast.warning(created.profileSyncError); } } catch (error) { toast.error( error instanceof Error ? error.message : "Failed to start agent.", ); } }, [createManagedAgentForPersona, resolvedPersona]); const handleToggleAgentAutoStart = React.useCallback(async () => { if (managedAgent?.backend.type !== "local") return; try { const updated = await startOnLaunchMutation.mutateAsync({ pubkey: managedAgent.pubkey, startOnAppLaunch: !managedAgent.startOnAppLaunch, }); toast.success( updated.startOnAppLaunch ? `Will start ${updated.name} automatically.` : `${updated.name} will stay manual-start only.`, ); } catch (error) { toast.error( error instanceof Error ? error.message : "Failed to update startup preference.", ); } }, [managedAgent, startOnLaunchMutation.mutateAsync]); const handleDeleteAgent = React.useCallback(async () => { if (!managedAgent) return; try { const result = await deleteManagedAgentRecord(managedAgent); if (result.cancelled) return; toast.success(`Deleted ${managedAgent.name}.`); onClose(); } catch (error) { toast.error( error instanceof Error ? error.message : "Failed to delete agent.", ); } }, [deleteManagedAgentRecord, managedAgent, onClose]); const handleSubmitPersona = React.useCallback( async (input: CreatePersonaInput | UpdatePersonaInput) => { try { if ("id" in input) { await updatePersonaMutation.mutateAsync(input); toast.success(`Updated ${input.displayName}.`); } else { const persona = await createPersonaMutation.mutateAsync(input); try { const created = await createManagedAgentForPersona(persona); if (created.spawnError) { toast.error( `${persona.displayName} was created, but it did not start: ${created.spawnError}`, ); } else { toast.success(`Created and started ${created.agent.name}.`); } if (created.profileSyncError) { toast.warning( `${created.agent.name} was created, but profile sync failed: ${created.profileSyncError}`, ); } } catch (error) { toast.error( error instanceof Error ? `${persona.displayName} was created, but the agent instance could not be created: ${error.message}` : `${persona.displayName} was created, but the agent instance could not be created.`, ); } } setPersonaDialogState(null); void personasQuery.refetch(); } catch (error) { toast.error( error instanceof Error ? error.message : "Failed to save agent.", ); } }, [ createPersonaMutation.mutateAsync, createManagedAgentForPersona, personasQuery.refetch, updatePersonaMutation.mutateAsync, ], ); const handleEditPersona = React.useCallback(() => { if (!resolvedPersona || resolvedPersona.isBuiltIn) return; setPersonaDialogState(editPersonaDialogState(resolvedPersona)); }, [resolvedPersona]); const handleDuplicatePersona = React.useCallback(() => { if (!resolvedPersona) return; setPersonaDialogState(duplicatePersonaDialogState(resolvedPersona)); }, [resolvedPersona]); const handleExportPersona = React.useCallback(() => { if (!resolvedPersona) return; exportPersonaJsonMutation.mutate(resolvedPersona.id, { onSuccess: (saved) => { if (saved) { toast.success(`Exported ${resolvedPersona.displayName}.`); } }, onError: (error) => { toast.error( error instanceof Error ? error.message : "Failed to export agent.", ); }, }); }, [exportPersonaJsonMutation, resolvedPersona]); const handleDeletePersona = React.useCallback(async () => { if (!resolvedPersona) return; if (resolvedPersona.isBuiltIn) { try { const deletedInstances = await deleteManagedAgentsForPersona(resolvedPersona); if (deletedInstances.cancelled) return; await setPersonaActiveMutation.mutateAsync({ id: resolvedPersona.id, active: false, }); toast.success(`Removed ${resolvedPersona.displayName} from My Agents.`); onClose(); } catch (error) { toast.error( error instanceof Error ? error.message : "Failed to delete agent.", ); } return; } if (resolvedPersona.sourceTeam) { toast.error("This agent is managed by a team."); return; } setPersonaToDelete(resolvedPersona); }, [ deleteManagedAgentsForPersona, onClose, resolvedPersona, setPersonaActiveMutation.mutateAsync, ]); const handleConfirmDeletePersona = React.useCallback( async (personaToConfirm: AgentPersona) => { try { const deletedInstances = await deleteManagedAgentsForPersona(personaToConfirm); if (deletedInstances.cancelled) return; await deletePersonaMutation.mutateAsync(personaToConfirm.id); toast.success(`Deleted ${personaToConfirm.displayName}.`); setPersonaToDelete(null); onClose(); } catch (error) { toast.error( error instanceof Error ? error.message : "Failed to delete agent.", ); } }, [deleteManagedAgentsForPersona, deletePersonaMutation.mutateAsync, onClose], ); const handleAddedToChannel = React.useCallback( (channel: Channel, result: AttachManagedAgentToChannelResult) => { if (result.restarted) { toast.success( `Added ${result.agent.name} to ${channel.name} and restarted it.`, ); } else if (result.started) { toast.success(`Added ${result.agent.name} to ${channel.name}.`); } else if (result.membershipAdded) { toast.success(`Added ${result.agent.name} to ${channel.name}.`); } else { toast.success(`${result.agent.name} is already in ${channel.name}.`); } void managedAgentsQuery.refetch(); void relayAgentsQuery.refetch(); void channelsQuery.refetch(); }, [ channelsQuery.refetch, managedAgentsQuery.refetch, relayAgentsQuery.refetch, ], ); const handleOpenActivity = React.useCallback(() => { if (!effectivePubkey) return; onClose(); onOpenAgentSession?.(effectivePubkey); }, [effectivePubkey, onClose, onOpenAgentSession]); const handleOpenChannel = React.useCallback( (channelId: string) => { void goChannel(channelId); }, [goChannel], ); const displayName = resolveProfileDisplayName({ persona: resolvedPersona, profile, pubkey: effectivePubkey, }); const ownerHandle = resolveOwnerHandle( currentProfileQuery.data, currentPubkey, ); const ownerDisplayName = ownerHandle ? `${ownerHandle} (you)` : null; const memoryCount = memoryQuery.data && (memoryQuery.data.core ? 1 : 0) + memoryQuery.data.memories.length; const agentInstruction = resolveAgentInstruction( managedAgent, resolvedPersona, ); const canManagePersona = isOwner === true && resolvedPersona !== undefined; const canEditPersona = canManagePersona && resolvedPersona?.isBuiltIn !== true; const canDeletePersona = canManagePersona && !resolvedPersona?.sourceTeam; const { agentInfoFields, agentSettingsFields, diagnosticsFields, diagnosticsSummary, modelLabel, } = useProfileFieldBuckets({ isBot, isOwner, managedAgent, ownerDisplayName, ownerHandle, persona: resolvedPersona, presenceLoaded: presenceQuery.isSuccess, presenceStatus, profile, pubkey: effectivePubkey, relayAgent, }); const headerLeftContent = ( {view !== "summary" ? ( ) : null} {PROFILE_PANEL_VIEW_TITLES[view]} ); const headerActions = (
{view === "memories" && isOwner === true && effectivePubkey ? ( ) : null}
); const profileBody = (
{view === "summary" ? ( setView("info")} onOpenAgentSettings={() => setView("settings")} onOpenChannels={() => setView("channels")} onOpenDiagnostics={() => setView("diagnostics")} onOpenInstruction={() => setView("instructions")} onOpenMemories={() => setView("memories")} onOpenModel={() => setView("model")} onOpenDm={onOpenDm} persona={resolvedPersona} presenceStatus={presenceStatus} profile={profile} pubkey={effectivePubkey} relayAgent={relayAgent} unfollowMutation={unfollowMutation} userStatus={userStatus} /> ) : null} {view === "memories" && effectivePubkey ? ( ) : null} {view === "instructions" ? ( ) : null} {view === "info" ? ( ) : null} {view === "model" ? ( void managedAgentsQuery.refetch()} /> ) : null} {view === "settings" ? ( ) : null} {view === "diagnostics" ? ( setView("logs")} pubkey={effectivePubkey} /> ) : null} {view === "channels" ? ( setAddToChannelOpen(true)} onOpenChannel={handleOpenChannel} /> ) : null} {view === "logs" ? ( ) : null}
); const editAgentDialog = canEditAgent && managedAgent ? ( ) : null; const addAgentToChannelDialog = managedAgent ? ( ) : null; const personaDialogs = ( setPersonaToDelete(null)} onCloseDialog={() => setPersonaDialogState(null)} onConfirmDelete={(selectedPersona) => { void handleConfirmDeletePersona(selectedPersona); }} onSubmit={handleSubmitPersona} /> ); if (isSplitLayout) { return ( <>
{headerLeftContent} {headerActions} {profileBody}
{editAgentDialog} {addAgentToChannelDialog} {personaDialogs} ); } return ( <> {isFloatingOverlay && } {editAgentDialog} {addAgentToChannelDialog} {personaDialogs} ); }