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,
},