diff --git a/autogpt_platform/backend/backend/api/features/library/db.py b/autogpt_platform/backend/backend/api/features/library/db.py index 4e9ac5bd2b..1eb224521a 100644 --- a/autogpt_platform/backend/backend/api/features/library/db.py +++ b/autogpt_platform/backend/backend/api/features/library/db.py @@ -87,10 +87,10 @@ async def list_library_agents( "isArchived": False, } - # Apply folder filter - if folder_id is not None: + # Apply folder filter (skip when searching — search spans all folders) + if folder_id is not None and not search_term: where_clause["folderId"] = folder_id - elif include_root_only: + elif include_root_only and not search_term: where_clause["folderId"] = None # Build search filter if applicable diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/LibraryAgentList.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/LibraryAgentList.tsx index fd5163d8e4..41ad465db0 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/LibraryAgentList.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/LibraryAgentList.tsx @@ -21,62 +21,57 @@ import { LibraryFolderEditDialog } from "../LibraryFolderEditDialog/LibraryFolde import { LibraryFolderDeleteDialog } from "../LibraryFolderDeleteDialog/LibraryFolderDeleteDialog"; import { useLibraryAgentList } from "./useLibraryAgentList"; +// Spring-based enter/exit animations (Emil Kowalski principles) +// Springs are naturally interruptible — switching tabs mid-animation +// cancels the current spring and starts a new one from current state. const containerVariants = { hidden: {}, - show: { - transition: { - staggerChildren: 0.04, - delayChildren: 0.04, - }, - }, + show: {}, exit: { opacity: 0, filter: "blur(4px)", - transition: { - duration: 0.15, - ease: [0.25, 0.1, 0.25, 1], - }, - }, -}; - -const itemVariants = { - hidden: { - opacity: 0, - y: 8, - scale: 0.96, - filter: "blur(4px)", - }, - show: { - opacity: 1, - y: 0, - scale: 1, - filter: "blur(0px)", - transition: { - duration: 0.25, - ease: [0.25, 0.1, 0.25, 1], - }, + transition: { duration: 0.12 }, }, }; +// Reduced motion fallback const reducedContainerVariants = { hidden: {}, - show: { - transition: { staggerChildren: 0.02 }, - }, + show: {}, exit: { opacity: 0, - transition: { duration: 0.15 }, + transition: { duration: 0.12 }, }, }; -const reducedItemVariants = { - hidden: { opacity: 0 }, - show: { - opacity: 1, - transition: { duration: 0.2 }, - }, +// Per-item animation values (explicit initial/animate, not variant-based). +// This ensures items animate in on mount regardless of parent state — fixes +// the bug where dynamically added children (e.g. folders reappearing after +// search is cleared) stayed invisible with variant inheritance. +const itemInitial = { + opacity: 0, + y: 8, + filter: "blur(4px)", }; +const itemAnimate = { + opacity: 1, + y: 0, + filter: "blur(0px)", +}; + +const itemTransition = { + type: "spring" as const, + stiffness: 300, + damping: 25, + opacity: { duration: 0.2 }, + filter: { duration: 0.15 }, +}; + +const reducedItemInitial = { opacity: 0 }; +const reducedItemAnimate = { opacity: 1 }; +const reducedItemTransition = { duration: 0.15 }; + interface Props { searchTerm: string; librarySort: LibraryAgentSort; @@ -102,9 +97,11 @@ export function LibraryAgentList({ const activeContainerVariants = shouldReduceMotion ? reducedContainerVariants : containerVariants; - const activeItemVariants = shouldReduceMotion - ? reducedItemVariants - : itemVariants; + const activeInitial = shouldReduceMotion ? reducedItemInitial : itemInitial; + const activeAnimate = shouldReduceMotion ? reducedItemAnimate : itemAnimate; + const activeTransition = shouldReduceMotion + ? reducedItemTransition + : itemTransition; const { isFavoritesTab, @@ -181,7 +178,7 @@ export function LibraryAgentList({ loader={} > - + {showFolders && - foldersData?.folders.map((folder) => ( - + foldersData?.folders.map((folder, i) => ( + ))} - {agents.map((agent) => ( - + {agents.map((agent, i) => ( + ))} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts index b9ef9ae594..65f22eef5c 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts @@ -19,7 +19,7 @@ import { import { useToast } from "@/components/molecules/Toast/use-toast"; import { useFavoriteAgents } from "../../hooks/useFavoriteAgents"; import { getQueryClient } from "@/lib/react-query/queryClient"; -import { useQueryClient } from "@tanstack/react-query"; +import { keepPreviousData, useQueryClient } from "@tanstack/react-query"; import { useEffect, useRef, useState } from "react"; interface Props { @@ -50,8 +50,6 @@ export function useLibraryAgentList({ null, ); - // --- Agent list fetching --- - const { data: agentsQueryData, fetchNextPage, @@ -112,10 +110,13 @@ export function useLibraryAgentList({ // --- Folders --- - const { data: foldersData } = useGetV2ListLibraryFolders(undefined, { + const { data: rawFoldersData } = useGetV2ListLibraryFolders(undefined, { query: { select: okData }, }); + // When searching, suppress folder data so only agent results show + const foldersData = searchTerm ? undefined : rawFoldersData; + const { mutate: moveAgentToFolder } = usePostV2BulkMoveAgents({ mutation: { onMutate: async ({ data }) => { @@ -126,9 +127,10 @@ export function useLibraryAgentList({ queryKey: getGetV2ListLibraryAgentsQueryKey(), }); - const previousFolders = queryClient.getQueriesData< - getV2ListLibraryFoldersResponseSuccess - >({ queryKey: getGetV2ListLibraryFoldersQueryKey() }); + const previousFolders = + queryClient.getQueriesData({ + queryKey: getGetV2ListLibraryFoldersQueryKey(), + }); if (data.folder_id) { queryClient.setQueriesData( diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolderCreationDialog/LibraryFolderCreationDialog.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolderCreationDialog/LibraryFolderCreationDialog.tsx index 7ac9fca52c..2fe0078896 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolderCreationDialog/LibraryFolderCreationDialog.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolderCreationDialog/LibraryFolderCreationDialog.tsx @@ -19,10 +19,8 @@ import { z } from "zod"; import { EmojiPicker } from "@ferrucc-io/emoji-picker"; import { usePostV2CreateFolder, - useGetV2ListLibraryFolders, getGetV2ListLibraryFoldersQueryKey, } from "@/app/api/__generated__/endpoints/folders/folders"; -import { okData } from "@/app/api/helpers"; import { useToast } from "@/components/molecules/Toast/use-toast"; import { useQueryClient } from "@tanstack/react-query"; @@ -45,10 +43,6 @@ export default function LibraryFolderCreationDialog() { const queryClient = useQueryClient(); const { toast } = useToast(); - const { data: foldersData } = useGetV2ListLibraryFolders(undefined, { - query: { select: okData }, - }); - const { mutate: createFolder, isPending } = usePostV2CreateFolder({ mutation: { onSuccess: () => { @@ -80,16 +74,6 @@ export default function LibraryFolderCreationDialog() { }); function onSubmit(values: z.infer) { - const existingNames = (foldersData?.folders ?? []).map((f) => - f.name.toLowerCase(), - ); - if (existingNames.includes(values.folderName.trim().toLowerCase())) { - form.setError("folderName", { - message: "A folder with this name already exists", - }); - return; - } - createFolder({ data: { name: values.folderName.trim(), diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolderEditDialog/LibraryFolderEditDialog.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolderEditDialog/LibraryFolderEditDialog.tsx index 1ff16652a7..a0c73b3aca 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolderEditDialog/LibraryFolderEditDialog.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolderEditDialog/LibraryFolderEditDialog.tsx @@ -20,10 +20,8 @@ import { z } from "zod"; import { EmojiPicker } from "@ferrucc-io/emoji-picker"; import { usePatchV2UpdateFolder, - useGetV2ListLibraryFolders, getGetV2ListLibraryFoldersQueryKey, } from "@/app/api/__generated__/endpoints/folders/folders"; -import { okData } from "@/app/api/helpers"; import { useQueryClient } from "@tanstack/react-query"; import type { LibraryFolder } from "@/app/api/__generated__/models/libraryFolder"; import type { getV2ListLibraryFoldersResponseSuccess } from "@/app/api/__generated__/endpoints/folders/folders"; @@ -52,10 +50,6 @@ export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) { const queryClient = useQueryClient(); const { toast } = useToast(); - const { data: foldersData } = useGetV2ListLibraryFolders(undefined, { - query: { select: okData }, - }); - const form = useForm>({ resolver: zodResolver(editFolderSchema), defaultValues: { @@ -154,22 +148,10 @@ export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) { }); function onSubmit(values: z.infer) { - const trimmedName = values.folderName.trim(); - const existingNames = (foldersData?.folders ?? []) - .filter((f) => f.id !== folder.id) - .map((f) => f.name.toLowerCase()); - - if (existingNames.includes(trimmedName.toLowerCase())) { - form.setError("folderName", { - message: "A folder with this name already exists", - }); - return; - } - updateFolder({ folderId: folder.id, data: { - name: trimmedName, + name: values.folderName.trim(), color: values.folderColor, icon: values.folderIcon, },