"use client"; import { useCallback, useEffect } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { toast } from "sonner"; import { useProjects } from "@/hooks/use-projects"; import { useGitStatus, useGitLog, useGitBranches, useGitDiff, useGitOperations, } from "@/hooks/use-git"; import { usePageRefresh } from "@/hooks/use-page-refresh"; import { getErrorMessage } from "@/lib/api/client"; import type { BranchType } from "@/types/git"; export interface UseGitBrowserResult { projectSlug: string; taskId: string; projects: ReturnType["data"]; loadingProjects: boolean; status: ReturnType["data"]; loadingStatus: boolean; log: ReturnType["data"]; loadingLog: boolean; branches: ReturnType["data"]; loadingBranches: boolean; stagedDiff: ReturnType["data"]; loadingStagedDiff: boolean; unstagedDiff: ReturnType["data"]; loadingUnstagedDiff: boolean; isOffline: boolean; refresh: () => Promise; handleProjectChange: (slug: string) => void; handleCheckout: (branch: string) => Promise; handleCreateBranch: ( branchType: BranchType, branchTaskId: string, ) => Promise; handleCommit: (message: string) => Promise; handlePush: (force?: boolean) => Promise; handleCreatePR: (title: string, body: string) => Promise; handleMergePR: (prNumber: number) => Promise; handlePull: () => Promise; handleFetch: () => Promise; handleRebase: (targetBranch: string) => Promise; isCommitting: boolean; isPushing: boolean; isCreatingPR: boolean; isMerging: boolean; isPulling: boolean; isFetching: boolean; isRebasing: boolean; isCheckingOut: boolean; isCreatingBranch: boolean; } /** * Fetches all data and binds all actions for the Git browser page. * * Keeping this logic out of `GitBrowserContent` lets the component remain * presentational and avoids the `thin_components` architectural-convention * warning. */ export function useGitBrowser(): UseGitBrowserResult { const router = useRouter(); const searchParams = useSearchParams(); const { register, unregister, refresh } = usePageRefresh(); const projectSlug = searchParams.get("project") || ""; const taskId = searchParams.get("task") || ""; const { data: projects, isLoading: loadingProjects, error: projectsError, refetch: refetchProjects, } = useProjects(); const { data: status, isLoading: loadingStatus, refetch: refetchStatus, } = useGitStatus(projectSlug, taskId, !!projectSlug); const { data: log, isLoading: loadingLog, refetch: refetchLog, } = useGitLog(projectSlug, 20, undefined, !!projectSlug); const { data: branches, isLoading: loadingBranches, refetch: refetchBranches, } = useGitBranches(projectSlug, true, !!projectSlug); const { data: stagedDiff, isLoading: loadingStagedDiff } = useGitDiff( projectSlug, true, undefined, !!projectSlug, ); const { data: unstagedDiff, isLoading: loadingUnstagedDiff } = useGitDiff( projectSlug, false, undefined, !!projectSlug, ); // Register all active git queries with the page-scoped refresh button. useEffect(() => { const callbacks = [ () => void refetchProjects(), () => void refetchStatus(), () => void refetchLog(), () => void refetchBranches(), ]; callbacks.forEach((cb) => register(cb)); return () => callbacks.forEach((cb) => unregister(cb)); }, [ register, unregister, refetchProjects, refetchStatus, refetchLog, refetchBranches, ]); const updateParams = useCallback( (updates: Record) => { const params = new URLSearchParams(searchParams.toString()); Object.entries(updates).forEach(([key, value]) => { if (value) { params.set(key, value); } else { params.delete(key); } }); const query = params.toString(); router.push(query ? `/git?${query}` : "/git"); }, [router, searchParams], ); const handleProjectChange = useCallback( (slug: string) => { updateParams({ project: slug || null, task: null }); }, [updateParams], ); const { commit, push, createBranch, checkout, createPR, mergePR, pull, fetch, rebase, } = useGitOperations(); const handleCheckout = useCallback( async (branch: string) => { try { await checkout.mutateAsync({ project_slug: projectSlug, branch, agent_id: "ceo", }); toast.success(`Checked out ${branch}`); } catch { toast.error("Failed to checkout branch"); } }, [projectSlug, checkout], ); const handleCreateBranch = useCallback( async (branchType: BranchType, branchTaskId: string) => { try { const result = await createBranch.mutateAsync({ project_slug: projectSlug, task_id: branchTaskId, branch_type: branchType, agent_id: "ceo", }); toast.success(`Created branch ${result.branch_name}`); } catch { toast.error("Failed to create branch"); } }, [projectSlug, createBranch], ); const handleCommit = useCallback( async (message: string) => { try { const result = await commit.mutateAsync({ project_slug: projectSlug, message, task_id: taskId || undefined, agent_id: "ceo", }); toast.success(`Committed: ${result.commit_hash.slice(0, 7)}`); } catch { toast.error("Failed to commit"); } }, [projectSlug, taskId, commit], ); const handlePush = useCallback( async (force?: boolean) => { try { const result = await push.mutateAsync({ project_slug: projectSlug, task_id: taskId || undefined, agent_id: "ceo", force, }); toast.success( `Pushed ${result.commits_pushed} commits to ${result.branch}`, ); } catch { toast.error("Failed to push"); } }, [projectSlug, taskId, push], ); const handleCreatePR = useCallback( async (title: string, body: string) => { try { const result = await createPR.mutateAsync({ project_slug: projectSlug, task_id: taskId || undefined, title, body, agent_id: "ceo", }); toast.success(`Created PR #${result.pr_number}: ${result.pr_url}`); } catch { toast.error("Failed to create PR"); } }, [projectSlug, taskId, createPR], ); const handleMergePR = useCallback( async (prNumber: number) => { try { const result = await mergePR.mutateAsync({ project_slug: projectSlug, pr_number: prNumber, task_id: taskId || undefined, agent_id: "ceo", }); toast.success( `Merged PR #${result.pr_number} → ${result.target_branch}`, ); } catch { toast.error("Failed to merge PR"); } }, [projectSlug, taskId, mergePR], ); const handlePull = useCallback(async () => { try { const result = await pull.mutateAsync({ project_slug: projectSlug, task_id: taskId || undefined, }); toast.success(`Pulled: now on ${result.current_branch}`); } catch { toast.error("Failed to pull from remote"); } }, [projectSlug, taskId, pull]); const handleFetch = useCallback(async () => { try { const result = await fetch.mutateAsync({ project_slug: projectSlug, task_id: taskId || undefined, }); toast.success(`Fetched: now on ${result.current_branch}`); } catch { toast.error("Failed to fetch from remote"); } }, [projectSlug, taskId, fetch]); const handleRebase = useCallback( async (targetBranch: string) => { try { const result = await rebase.mutateAsync({ project_slug: projectSlug, target_branch: targetBranch, task_id: taskId || undefined, agent_id: "ceo", }); if (result.conflict) { toast.warning( `Rebase conflicts in: ${result.conflicted_files.join(", ") || "unknown files"}`, ); } else { toast.success("Rebase completed successfully"); } } catch (error) { toast.error(getErrorMessage(error)); } }, [projectSlug, taskId, rebase], ); const isOffline = !!projectsError && (projectsError.message?.includes("Network Error") || (projectsError as { code?: string }).code === "ERR_NETWORK"); return { projectSlug, taskId, projects, loadingProjects, status, loadingStatus, log, loadingLog, branches, loadingBranches, stagedDiff, loadingStagedDiff, unstagedDiff, loadingUnstagedDiff, isOffline, refresh, handleProjectChange, handleCheckout, handleCreateBranch, handleCommit, handlePush, handleCreatePR, handleMergePR, handlePull, handleFetch, handleRebase, isCommitting: commit.isPending, isPushing: push.isPending, isCreatingPR: createPR.isPending, isMerging: mergePR.isPending, isPulling: pull.isPending, isFetching: fetch.isPending, isRebasing: rebase.isPending, isCheckingOut: checkout.isPending, isCreatingBranch: createBranch.isPending, }; }