diff --git a/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx b/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx index 97d87afd60..d22825d8d1 100644 --- a/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx +++ b/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx @@ -157,8 +157,52 @@ describe("MicroagentManagement", () => { owner_type: "organization", pushed_at: "2021-10-06T12:00:00Z", }, + { + id: "7", + full_name: "user/gitlab-repo/openhands-config", + git_provider: "gitlab", + is_public: true, + owner_type: "user", + pushed_at: "2021-10-07T12:00:00Z", + }, + { + id: "8", + full_name: "org/gitlab-org-repo/openhands-config", + git_provider: "gitlab", + is_public: true, + owner_type: "organization", + pushed_at: "2021-10-08T12:00:00Z", + }, ]; + // Helper function to filter repositories with OpenHands suffixes + const getRepositoriesWithOpenHandsSuffix = ( + repositories: GitRepository[], + ) => { + return repositories.filter( + (repo) => + repo.full_name.endsWith("/.openhands") || + repo.full_name.endsWith("/openhands-config"), + ); + }; + + // Helper functions for mocking search repositories + const mockSearchRepositoriesWithData = (data: GitRepository[]) => { + mockUseSearchRepositories.mockReturnValue({ + data, + isLoading: false, + isError: false, + }); + }; + + const mockSearchRepositoriesEmpty = () => { + mockUseSearchRepositories.mockReturnValue({ + data: [], + isLoading: false, + isError: false, + }); + }; + const mockMicroagents: RepositoryMicroagent[] = [ { name: "test-microagent-1", @@ -265,11 +309,11 @@ describe("MicroagentManagement", () => { isError: false, }); - mockUseSearchRepositories.mockReturnValue({ - data: [], - isLoading: false, - isError: false, - }); + // Mock the search repositories hook to return repositories with OpenHands suffixes + const mockSearchResults = + getRepositoriesWithOpenHandsSuffix(mockRepositories); + + mockSearchRepositoriesWithData(mockSearchResults); // Setup default mock for retrieveUserGitRepositories vi.spyOn(GitService, "retrieveUserGitRepositories").mockResolvedValue({ @@ -594,6 +638,9 @@ describe("MicroagentManagement", () => { onLoadMore: vi.fn(), }); + // Mock empty search results + mockSearchRepositoriesEmpty(); + renderMicroagentManagement(); // Wait for repositories to be loaded @@ -782,6 +829,10 @@ describe("MicroagentManagement", () => { it("should handle empty search results", async () => { const user = userEvent.setup(); + + // Mock empty search results for this test + mockSearchRepositoriesEmpty(); + renderMicroagentManagement(); // Wait for repositories to be loaded diff --git a/frontend/src/components/features/microagent-management/microagent-management-sidebar.tsx b/frontend/src/components/features/microagent-management/microagent-management-sidebar.tsx index b8f6271504..3ef952bb79 100644 --- a/frontend/src/components/features/microagent-management/microagent-management-sidebar.tsx +++ b/frontend/src/components/features/microagent-management/microagent-management-sidebar.tsx @@ -9,7 +9,12 @@ import { GitProviderDropdown } from "#/components/features/home/git-provider-dro import { useMicroagentManagementStore } from "#/state/microagent-management-store"; import { GitRepository } from "#/types/git"; import { Provider } from "#/types/settings"; -import { cn } from "#/utils/utils"; +import { + cn, + shouldIncludeRepository, + getOpenHandsQuery, + hasOpenHandsSuffix, +} from "#/utils/utils"; import { sanitizeQuery } from "#/utils/sanitize-query"; import { I18nKey } from "#/i18n/declaration"; import { useDebounce } from "#/hooks/use-debounce"; @@ -55,6 +60,16 @@ export function MicroagentManagementSidebar({ const { data: searchResults, isLoading: isSearchLoading } = useSearchRepositories(debouncedSearchQuery, selectedProvider, false, 500); // Increase page size to 500 to to retrieve all search results. This should be optimized in the future. + const { + data: userAndOrgLevelRepositorySearchResults, + isLoading: isUserAndOrgLevelRepositoryLoading, + } = useSearchRepositories( + getOpenHandsQuery(selectedProvider), + selectedProvider, + false, + 500, + ); + // Auto-select provider if there's only one useEffect(() => { if (providers.length > 0 && !selectedProvider) { @@ -100,43 +115,61 @@ export function MicroagentManagementSidebar({ return filterRepositoriesByQuery(allRepositories, debouncedSearchQuery); }, [repositories, debouncedSearchQuery, searchResults]); + // Process personal and organization repositories from search results useEffect(() => { - if (!filteredRepositories?.length) { + if (!userAndOrgLevelRepositorySearchResults?.length) { setPersonalRepositories([]); setOrganizationRepositories([]); - setRepositories([]); return; } const personalRepos: GitRepository[] = []; const organizationRepos: GitRepository[] = []; + + // Process personal repositories with exact match filtering + if (userAndOrgLevelRepositorySearchResults?.length) { + userAndOrgLevelRepositorySearchResults.forEach((repo: GitRepository) => { + if ( + hasOpenHandsSuffix(repo, selectedProvider) && + shouldIncludeRepository(repo, debouncedSearchQuery) + ) { + if (repo.owner_type === "user") { + personalRepos.push(repo); + } else if (repo.owner_type === "organization") { + organizationRepos.push(repo); + } + } + }); + } + + setPersonalRepositories(personalRepos); + setOrganizationRepositories(organizationRepos); + }, [ + userAndOrgLevelRepositorySearchResults, + selectedProvider, + debouncedSearchQuery, + setPersonalRepositories, + setOrganizationRepositories, + ]); + + // Process other repositories (non-OpenHands repositories) from filteredRepositories + useEffect(() => { + if (!filteredRepositories?.length) { + setRepositories([]); + return; + } + const otherRepos: GitRepository[] = []; filteredRepositories.forEach((repo: GitRepository) => { - const hasOpenHandsSuffix = - selectedProvider === "gitlab" - ? repo.full_name.endsWith("/openhands-config") - : repo.full_name.endsWith("/.openhands"); - - if (repo.owner_type === "user" && hasOpenHandsSuffix) { - personalRepos.push(repo); - } else if (repo.owner_type === "organization" && hasOpenHandsSuffix) { - organizationRepos.push(repo); - } else { + // Only include repositories that don't have the OpenHands suffix + if (!hasOpenHandsSuffix(repo, selectedProvider)) { otherRepos.push(repo); } }); - setPersonalRepositories(personalRepos); - setOrganizationRepositories(organizationRepos); setRepositories(otherRepos); - }, [ - filteredRepositories, - selectedProvider, - setPersonalRepositories, - setOrganizationRepositories, - setRepositories, - ]); + }, [filteredRepositories, selectedProvider, setRepositories]); // Handle scroll to bottom for pagination const handleScroll = (event: React.UIEvent) => { @@ -206,7 +239,7 @@ export function MicroagentManagementSidebar({ - {isLoading ? ( + {isLoading || isUserAndOrgLevelRepositoryLoading ? (
diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts index fafbe6e4b5..bc990e0ccf 100644 --- a/frontend/src/utils/utils.ts +++ b/frontend/src/utils/utils.ts @@ -3,6 +3,8 @@ import { twMerge } from "tailwind-merge"; import { Provider } from "#/types/settings"; import { SuggestedTaskGroup } from "#/utils/types"; import { ConversationStatus } from "#/types/conversation-status"; +import { GitRepository } from "#/types/git"; +import { sanitizeQuery } from "#/utils/sanitize-query"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -509,3 +511,50 @@ export const getStatusClassName = (status: string) => { } return "bg-gray-700 text-gray-300"; }; + +/** + * Helper function to apply client-side filtering based on search query + * @param repo The Git repository to check + * @param searchQuery The search query string + * @returns True if the repository should be included based on the search query + */ +export const shouldIncludeRepository = ( + repo: GitRepository, + searchQuery: string, +): boolean => { + if (!searchQuery.trim()) { + return true; + } + + const sanitizedQuery = sanitizeQuery(searchQuery); + const sanitizedRepoName = sanitizeQuery(repo.full_name); + return sanitizedRepoName.includes(sanitizedQuery); +}; + +/** + * Get the OpenHands query string based on the provider + * @param provider The git provider + * @returns The query string for searching OpenHands repositories + */ +export const getOpenHandsQuery = (provider: Provider | null): string => { + if (provider === "gitlab") { + return "openhands-config"; + } + return ".openhands"; +}; + +/** + * Check if a repository has the OpenHands suffix based on the provider + * @param repo The Git repository to check + * @param provider The git provider + * @returns True if the repository has the OpenHands suffix + */ +export const hasOpenHandsSuffix = ( + repo: GitRepository, + provider: Provider | null, +): boolean => { + if (provider === "gitlab") { + return repo.full_name.endsWith("/openhands-config"); + } + return repo.full_name.endsWith("/.openhands"); +};