import { useCallback, useEffect, useMemo } from "react"; import type { EnvironmentId, OrchestrationCheckpointSummary, ThreadId } from "@t3tools/contracts"; import { useCheckpointDiff } from "../../state/queries"; import { useEnvironmentQuery } from "../../state/query"; import { reviewEnvironment } from "../../state/review"; import { useSelectedThreadDetail } from "../../state/use-thread-detail"; import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree"; import { buildReviewSectionItems, getDefaultReviewSectionId, getReadyReviewCheckpoints, getReviewSectionIdForCheckpoint, } from "./reviewModel"; import { setReviewAsyncError, setReviewGitSections, setReviewSelectedSectionId, setReviewTurnDiff, setReviewTurnDiffLoading, type ReviewCacheForThread, } from "./reviewState"; export function useReviewSections(input: { readonly enabled?: boolean; readonly environmentId?: EnvironmentId; readonly threadId?: ThreadId; readonly reviewCache: ReviewCacheForThread; }) { const { environmentId, reviewCache, threadId } = input; const enabled = input.enabled ?? true; const selectedThread = useSelectedThreadDetail(); const { selectedThreadCwd } = useSelectedThreadWorktree(); const diffPreview = useEnvironmentQuery( enabled && environmentId !== undefined && selectedThreadCwd !== null ? reviewEnvironment.diffPreview({ environmentId, input: { cwd: selectedThreadCwd }, }) : null, ); const { loadingTurnIds } = reviewCache.asyncState; useEffect(() => { if (reviewCache.threadKey && diffPreview.data) { setReviewGitSections(reviewCache.threadKey, diffPreview.data.sources); } }, [diffPreview.data, reviewCache.threadKey]); const readyCheckpoints = useMemo( () => getReadyReviewCheckpoints(selectedThread?.checkpoints ?? []), [selectedThread?.checkpoints], ); const checkpointBySectionId = useMemo( () => Object.fromEntries( readyCheckpoints.map((checkpoint) => [ getReviewSectionIdForCheckpoint(checkpoint), checkpoint, ]), ) as Record, [readyCheckpoints], ); const reviewSections = useMemo( () => buildReviewSectionItems({ checkpoints: readyCheckpoints, gitSections: reviewCache.gitSections, turnDiffById: reviewCache.turnDiffById, loadingTurnIds, loadingGitSections: diffPreview.isPending, }), [ diffPreview.isPending, loadingTurnIds, readyCheckpoints, reviewCache.gitSections, reviewCache.turnDiffById, ], ); const selectedSection = useMemo( () => reviewSections.find((section) => section.id === reviewCache.selectedSectionId) ?? reviewSections[0] ?? null, [reviewCache.selectedSectionId, reviewSections], ); const fallbackSectionId = useMemo( () => getDefaultReviewSectionId(reviewSections), [reviewSections], ); const selectedSectionIdExists = useMemo( () => reviewCache.selectedSectionId ? reviewSections.some((section) => section.id === reviewCache.selectedSectionId) : false, [reviewCache.selectedSectionId, reviewSections], ); useEffect(() => { if ( reviewSections.length > 0 && reviewCache.threadKey && (!reviewCache.selectedSectionId || !selectedSectionIdExists) ) { setReviewSelectedSectionId(reviewCache.threadKey, fallbackSectionId); } }, [ fallbackSectionId, reviewCache.selectedSectionId, reviewCache.threadKey, reviewSections.length, selectedSectionIdExists, ]); let activeCheckpoint = readyCheckpoints[0] ?? null; if (selectedSection?.kind === "turn") { activeCheckpoint = checkpointBySectionId[selectedSection.id] ?? activeCheckpoint; } const activeSectionId = activeCheckpoint ? getReviewSectionIdForCheckpoint(activeCheckpoint) : null; const activeTurnDiff = useCheckpointDiff({ environmentId: enabled ? (environmentId ?? null) : null, threadId: enabled ? (threadId ?? null) : null, fromTurnCount: enabled && activeCheckpoint ? Math.max(0, activeCheckpoint.checkpointTurnCount - 1) : null, toTurnCount: enabled ? (activeCheckpoint?.checkpointTurnCount ?? null) : null, ignoreWhitespace: false, }); useEffect(() => { if (!reviewCache.threadKey || !activeSectionId) { return; } setReviewTurnDiffLoading(reviewCache.threadKey, activeSectionId, activeTurnDiff.isPending); }, [activeSectionId, activeTurnDiff.isPending, reviewCache.threadKey]); useEffect(() => { if (!reviewCache.threadKey || !activeSectionId || !activeTurnDiff.data) { return; } setReviewTurnDiff(reviewCache.threadKey, activeSectionId, activeTurnDiff.data.diff); setReviewAsyncError(reviewCache.threadKey, null); }, [activeSectionId, activeTurnDiff.data, reviewCache.threadKey]); useEffect(() => { if (reviewCache.threadKey && activeTurnDiff.error) { setReviewAsyncError(reviewCache.threadKey, activeTurnDiff.error); } }, [activeTurnDiff.error, reviewCache.threadKey]); const refreshSelectedSection = useCallback(async () => { if (!enabled) { return; } if (selectedSection?.kind === "turn") { activeTurnDiff.refresh(); return; } diffPreview.refresh(); }, [activeTurnDiff, diffPreview, enabled, selectedSection?.kind]); const selectSection = useCallback( (sectionId: string) => { if (reviewCache.threadKey) { setReviewSelectedSectionId(reviewCache.threadKey, sectionId); } }, [reviewCache.threadKey], ); return { error: diffPreview.error ?? activeTurnDiff.error ?? reviewCache.asyncState.error, loadingGitDiffs: diffPreview.isPending, loadingTurnIds, reviewSections, selectedSection, refreshSelectedSection, selectSection, }; }