import { useInfiniteQuery, useMutation, useQuery, useQueryClient, } from "@tanstack/react-query" import { useNavigate } from "@tanstack/react-router" import { type UIEvent, useEffect, useRef, useState } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" import { type SkillRegistrySearchResult, type SkillSearchResponse, type SkillSupportItem, getSkills, installSkill, searchSkills, } from "@/api/skills" import { getTools } from "@/api/tools" import { buildUnavailableToolMessages } from "./tool-support" const MARKET_SEARCH_LIMIT = 20 export function useHubMarketplace() { const { t } = useTranslation() const navigate = useNavigate() const queryClient = useQueryClient() const isLoadMoreLockedRef = useRef(false) const [marketQuery, setMarketQuery] = useState("") const [submittedMarketQuery, setSubmittedMarketQuery] = useState("") const { data: skillsData } = useQuery({ queryKey: ["skills"], queryFn: getSkills, }) const { data: toolsData } = useQuery({ queryKey: ["tools"], queryFn: getTools, }) const findSkillsTool = toolsData?.tools.find( (tool) => tool.name === "find_skills", ) const installSkillTool = toolsData?.tools.find( (tool) => tool.name === "install_skill", ) const canSearchMarketplace = findSkillsTool?.status === "enabled" const canInstallFromMarketplace = installSkillTool?.status === "enabled" const hasSubmittedQuery = submittedMarketQuery.trim() !== "" const isMarketSearchActive = canSearchMarketplace && hasSubmittedQuery const { data: marketSearchData, isPending: isMarketSearchPending, isFetching: isMarketSearchFetching, isFetchingNextPage, error: marketSearchError, hasNextPage, fetchNextPage, refetch: refetchMarketSearch, } = useInfiniteQuery({ queryKey: ["skills-marketplace", submittedMarketQuery], initialPageParam: 0, queryFn: ({ pageParam }) => searchSkills( submittedMarketQuery, MARKET_SEARCH_LIMIT, Number(pageParam) || 0, ), getNextPageParam: (lastPage: SkillSearchResponse) => lastPage.has_more ? (lastPage.next_offset ?? undefined) : undefined, enabled: isMarketSearchActive, staleTime: 5 * 60 * 1000, refetchOnMount: false, refetchOnWindowFocus: false, }) const installMutation = useMutation({ mutationFn: installSkill, onSuccess: (response) => { toast.success( t("pages.agent.skills.install_success", { name: response.skill?.name ?? response.slug, }), ) void queryClient.invalidateQueries({ queryKey: ["skills"] }) void queryClient.invalidateQueries({ queryKey: ["skills-marketplace"] }) }, onError: (err) => { toast.error( err instanceof Error ? err.message : t("pages.agent.skills.install_error"), ) }, }) const allSkills = skillsData?.skills ?? [] const workspaceSkillsByName = new Map( allSkills .filter((skill) => skill.source === "workspace") .map((skill) => [skill.name, skill] as const), ) const marketResults = marketSearchData?.pages.flatMap((page) => page.results) ?? [] const hasMoreMarketResults = hasNextPage ?? false const isMarketSearchInitialLoading = isMarketSearchActive && !marketSearchData && (isMarketSearchPending || isMarketSearchFetching) const isMarketSearchLoadingMore = isMarketSearchActive && Boolean(marketSearchData) && isFetchingNextPage const installPendingKey = installMutation.isPending && installMutation.variables ? `${installMutation.variables.registry}:${installMutation.variables.slug}` : null const unavailableToolMessages = buildUnavailableToolMessages({ searchTool: findSkillsTool, installTool: installSkillTool, t, }) useEffect(() => { if (!isFetchingNextPage) { isLoadMoreLockedRef.current = false } }, [isFetchingNextPage]) const handleSearchSubmit = () => { const nextQuery = marketQuery.trim() if (!canSearchMarketplace || nextQuery === "") { return } isLoadMoreLockedRef.current = false if (nextQuery === submittedMarketQuery) { void refetchMarketSearch() return } setSubmittedMarketQuery(nextQuery) } const handleInstall = (result: SkillRegistrySearchResult) => { installMutation.mutate({ slug: result.slug, registry: result.registry_name, version: result.version || undefined, }) } const handleViewInstalled = () => { void navigate({ to: "/agent/skills" }) } const handleScroll = (event: UIEvent) => { if ( !isMarketSearchActive || !hasMoreMarketResults || isFetchingNextPage || isLoadMoreLockedRef.current ) { return } const node = event.currentTarget const remaining = node.scrollHeight - node.scrollTop - node.clientHeight if (remaining > 240) { return } isLoadMoreLockedRef.current = true void fetchNextPage() } const getInstalledSkill = ( installedName?: string, ): SkillSupportItem | null => { if (!installedName) { return null } return workspaceSkillsByName.get(installedName) ?? null } const isInstallPending = (result: SkillRegistrySearchResult) => installPendingKey === `${result.registry_name}:${result.slug}` return { marketQuery, submittedMarketQuery, canSearchMarketplace, canInstallFromMarketplace, marketResults, marketSearchError, unavailableToolMessages, hasSubmittedQuery, isMarketSearchInitialLoading, isMarketSearchLoadingMore, setMarketQuery, handleSearchSubmit, handleInstall, handleViewInstalled, handleScroll, getInstalledSkill, isInstallPending, } }