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<string, OrchestrationCheckpointSummary>, [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, };}
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<string, OrchestrationCheckpointSummary>,
[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,
};
}