import { RefreshIcon } from "~/components/ui/refresh-icon";import { useAtomValue } from "@effect/atom-react";import type { FileDiffContentsLoader } from "@pierre/diffs";import { useParams } from "@tanstack/react-router";import { isAtomCommandInterrupted, squashAtomCommandFailure,} from "@t3tools/client-runtime/state/runtime";import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";import type { ScopedThreadRef, TurnId } from "@t3tools/contracts";import { ArrowRightIcon, CheckIcon, ChevronDownIcon, ChevronRightIcon, ChevronsDownUpIcon, ChevronsUpDownIcon, Columns2Icon, FolderTreeIcon, PilcrowIcon, Rows3Icon, SearchIcon, TextWrapIcon,} from "lucide-react";import * as Schema from "effect/Schema";import { useCallback, useEffect, useMemo, useRef, useState } from "react";import { useCodeViewFileReveal } from "./diffs/useCodeViewFileReveal";import { useOpenInPreferredEditor } from "../editorPreferences";import { type DraftId } from "../composerDraftStore";import { openDiffFilePrimaryAction } from "../diffFileActions";import { useCheckpointDiff } from "~/lib/checkpointDiffState";import { cn } from "~/lib/utils";import { selectThreadDiffPanelSelection, useDiffPanelStore } from "../diffPanelStore";import { useLocalStorage } from "../hooks/useLocalStorage";import { useTheme } from "../hooks/useTheme";import { buildFileDiffContentVersion, buildFileDiffIdentityKey, getDiffCollapseIconClassName, getDiffLineStat, getRenderablePatch, resolveDiffThemeName, resolveFileDiffPath,} from "../lib/diffRendering";import { PREFERRED_HIGHLIGHTER } from "../lib/syntaxHighlighting";import { areAllDiffFilesCollapsed, toggleAllDiffFiles } from "../lib/diffCollapse";import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";import { useWorkspaceMutationRefresh } from "../hooks/useWorkspaceMutationRefresh";import { useProject, useThread } from "../state/entities";import { resolveThreadRouteRef } from "../threadRoutes";import { useClientSettings, useUpdateClientSettings } from "../hooks/useSettings";import { formatShortTimestamp } from "../timestampFormat";import { DiffFilePathCopyButton } from "./DiffFilePathCopyButton";import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell";import { DiffStatLabel } from "./chat/DiffStatLabel";import { AnnotatableCodeView, type AnnotatableCodeViewHandle } from "./diffs/AnnotatableCodeView";import { DiffFileTree } from "./diffs/DiffFileTree";import { diffFileTreeEntries } from "./diffs/diffFileTree.logic";import { Button } from "./ui/button";import { ToggleGroup, Toggle } from "./ui/toggle-group";import { Switch } from "./ui/switch";import { Combobox, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, ComboboxPopup, ComboboxTrigger,} from "./ui/combobox";import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger,} from "./ui/menu";import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";import { useEnvironmentQuery } from "../state/query";import { useAtomCommand } from "../state/use-atom-command";import { serverEnvironment } from "../state/server";import { reviewEnvironment } from "../state/review";import { vcsEnvironment } from "../state/vcs";import { buildBaseRefChoices, filterBaseRefChoices } from "../lib/baseRefChoices";import { createGitDiffFileContentsLoader } from "../lib/diffFileContents"; type DiffThemeType = "light" | "dark";const AUTOMATIC_BASE_REF = "__automatic_base_ref__";const DIFF_FILE_TREE_STORAGE_KEY = "t3code.diffFileTreeOpen"; interface CollapsedDiffFilesState { readonly scopeKey: string | null; readonly fileKeys: ReadonlySet<string>;} const EMPTY_COLLAPSED_DIFF_FILE_KEYS: ReadonlySet<string> = new Set(); interface DiffPanelProps { mode?: DiffPanelMode; composerDraftTarget: ScopedThreadRef | DraftId; initialGitScope: "branch" | "unstaged"; workspaceMutationId: string | null;} export default function DiffPanel({ mode = "inline", composerDraftTarget, initialGitScope: initialGitScopeProp, workspaceMutationId,}: DiffPanelProps) { const { resolvedTheme } = useTheme(); const settings = useClientSettings(); const [initialGitScope] = useState(initialGitScopeProp); const diffLayout = settings.diffLayout; const updateClientSettings = useUpdateClientSettings(); const [wordWrap, setWordWrap] = useState(settings.wordWrap); const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace); const [fileTreeOpen, setFileTreeOpen] = useLocalStorage( DIFF_FILE_TREE_STORAGE_KEY, false, Schema.Boolean, ); const [baseRefQuery, setBaseRefQuery] = useState(""); const [collapsedDiffFiles, setCollapsedDiffFiles] = useState<CollapsedDiffFilesState>(() => ({ scopeKey: null, fileKeys: EMPTY_COLLAPSED_DIFF_FILE_KEYS, })); const [codeViewRevision, setCodeViewRevision] = useState(0); const [codeView, setCodeView] = useState<AnnotatableCodeViewHandle | null>(null); const routeThreadRef = useParams({ strict: false, select: (params) => resolveThreadRouteRef(params), }); const activeThreadId = routeThreadRef?.threadId ?? null; const activeThread = useThread(routeThreadRef); const activeProjectId = activeThread?.projectId ?? null; const activeProject = useProject( activeThread && activeProjectId ? { environmentId: activeThread.environmentId, projectId: activeProjectId, } : null, ); const activeCwd = activeThread?.worktreePath ?? activeProject?.workspaceRoot; const activeRepositoryRoot = activeThread?.worktreePath ? undefined : activeProject?.repositoryIdentity?.rootPath; const serverConfig = useAtomValue( serverEnvironment.configValueAtom(activeThread?.environmentId ?? null), ); const openInPreferredEditor = useOpenInPreferredEditor( activeThread?.environmentId ?? null, serverConfig?.availableEditors ?? [], ); const getDiffFileContents = useAtomCommand(reviewEnvironment.diffFileContents); const gitStatusQuery = useEnvironmentQuery( activeThread !== null && activeThread !== undefined && activeCwd != null ? vcsEnvironment.status({ environmentId: activeThread.environmentId, input: { cwd: activeCwd }, }) : null, ); const diffSelection = useDiffPanelStore((state) => selectThreadDiffPanelSelection( state.byThreadKey, routeThreadRef, initialGitScope === "unstaged", ), ); const isGitRepo = gitStatusQuery.data?.isRepo ?? true; const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } = useTurnDiffSummaries(activeThread); const orderedTurnDiffSummaries = useMemo( () => [...turnDiffSummaries].toSorted((left, right) => { const leftTurnCount = left.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[left.turnId] ?? 0; const rightTurnCount = right.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[right.turnId] ?? 0; if (leftTurnCount !== rightTurnCount) { return rightTurnCount - leftTurnCount; } return right.completedAt.localeCompare(left.completedAt); }), [inferredCheckpointTurnCountByTurnId, turnDiffSummaries], ); useEffect(() => { if (!routeThreadRef || diffSelection.kind !== "turn") return; useDiffPanelStore.getState().reconcileTurnSelection( routeThreadRef, orderedTurnDiffSummaries.map((summary) => summary.turnId), ); }, [diffSelection, orderedTurnDiffSummaries, routeThreadRef]); const selectedTurnId = diffSelection.kind === "turn" ? diffSelection.turnId : null; const selectedGitScope = diffSelection.kind === "unstaged" ? "unstaged" : "branch"; const selectedBaseRef = diffSelection.kind === "branch" ? diffSelection.baseRef : null; const selectedFilePath = diffSelection.kind === "turn" ? diffSelection.filePath : null; const selectedFileRevealRequestId = diffSelection.kind === "turn" ? diffSelection.revealRequestId : 0; const selectedTurn = selectedTurnId === null ? undefined : (orderedTurnDiffSummaries.find((summary) => summary.turnId === selectedTurnId) ?? orderedTurnDiffSummaries[0]); const selectedCheckpointTurnCount = selectedTurn && (selectedTurn.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[selectedTurn.turnId]); const latestTurn = orderedTurnDiffSummaries[0]; const selectedScopeLabel = selectedTurnId === null ? selectedGitScope === "unstaged" ? "Working tree" : "Branch changes" : selectedTurn?.turnId === latestTurn?.turnId ? "Latest turn" : `Turn ${selectedCheckpointTurnCount ?? "?"}`; const reviewSectionId = selectedTurn ? `turn:${selectedTurn.turnId}` : selectedGitScope; const collapseScopeKey = routeThreadRef ? `${routeThreadRef.environmentId}:${routeThreadRef.threadId}:${reviewSectionId}` : null; const codeViewMountKey = `${collapseScopeKey ?? reviewSectionId}:${codeViewRevision}`; const collapsedDiffFileKeys = collapsedDiffFiles.scopeKey === collapseScopeKey ? collapsedDiffFiles.fileKeys : EMPTY_COLLAPSED_DIFF_FILE_KEYS; const reviewSectionTitle = selectedTurn ? `Turn ${selectedCheckpointTurnCount ?? "?"}` : selectedGitScope === "unstaged" ? "Working tree" : "Branch changes"; const selectedCheckpointRange = useMemo( () => typeof selectedCheckpointTurnCount === "number" ? { fromTurnCount: Math.max(0, selectedCheckpointTurnCount - 1), toTurnCount: selectedCheckpointTurnCount, } : null, [selectedCheckpointTurnCount], ); const activeCheckpointDiff = useCheckpointDiff( { environmentId: activeThread?.environmentId ?? null, threadId: activeThreadId, fromTurnCount: selectedCheckpointRange?.fromTurnCount ?? null, toTurnCount: selectedCheckpointRange?.toTurnCount ?? null, ignoreWhitespace: diffIgnoreWhitespace, cacheScope: selectedTurn ? `turn:${selectedTurn.turnId}` : null, }, { enabled: isGitRepo && selectedTurn !== undefined }, ); const primaryBranchDiffPreview = useEnvironmentQuery( selectedTurnId === null && activeThread && activeCwd ? reviewEnvironment.diffPreview({ environmentId: activeThread.environmentId, input: { cwd: activeCwd, ...(selectedBaseRef ? { baseRef: selectedBaseRef } : {}), ignoreWhitespace: diffIgnoreWhitespace, }, }) : null, ); const shouldRetryBranchDiffAtEnvironmentCwd = selectedTurnId === null && primaryBranchDiffPreview.error?.includes("configured workspace root") === true && serverConfig?.cwd !== undefined && serverConfig.cwd !== activeCwd; const fallbackBranchDiffPreview = useEnvironmentQuery( shouldRetryBranchDiffAtEnvironmentCwd && activeThread && serverConfig ? reviewEnvironment.diffPreview({ environmentId: activeThread.environmentId, input: { cwd: serverConfig.cwd, ...(selectedBaseRef ? { baseRef: selectedBaseRef } : {}), ignoreWhitespace: diffIgnoreWhitespace, }, }) : null, ); const branchDiffPreview = shouldRetryBranchDiffAtEnvironmentCwd ? fallbackBranchDiffPreview : primaryBranchDiffPreview; const refreshBranchDiffPreview = branchDiffPreview.refresh; const canRefreshGitDiff = isGitRepo && selectedTurnId === null && activeThread != null && activeCwd != null; const activeThreadRefreshKey = routeThreadRef ? `${routeThreadRef.environmentId}:${routeThreadRef.threadId}` : null; useEffect(() => { if (!canRefreshGitDiff) return; const refreshOnFocus = () => refreshBranchDiffPreview(); window.addEventListener("focus", refreshOnFocus); return () => window.removeEventListener("focus", refreshOnFocus); }, [canRefreshGitDiff, refreshBranchDiffPreview]); useWorkspaceMutationRefresh({ enabled: canRefreshGitDiff, mutationId: workspaceMutationId, refresh: refreshBranchDiffPreview, resourceKey: `diff:${activeThreadRefreshKey ?? ""}`, }); const selectedGitSource = branchDiffPreview.data?.sources.find( (source) => source.kind === (selectedGitScope === "unstaged" ? "working-tree" : "branch-range"), ); const currentLoadDiffFiles = useMemo<FileDiffContentsLoader | undefined>(() => { const preview = branchDiffPreview.data; if (selectedTurnId !== null || !activeThread || !preview || !selectedGitSource) { return undefined; } return createGitDiffFileContentsLoader(getDiffFileContents, { environmentId: activeThread.environmentId, cwd: preview.cwd, sourceKind: selectedGitSource.kind, baseRef: selectedGitSource.baseRef, headRef: selectedGitSource.headRef, cacheKey: selectedGitSource.diffHash, }); }, [ activeThread, branchDiffPreview.data, getDiffFileContents, selectedGitSource, selectedTurnId, ]); const loadDiffFilesRef = useRef(currentLoadDiffFiles); loadDiffFilesRef.current = currentLoadDiffFiles; const loadDiffFiles = useCallback<FileDiffContentsLoader>(async (fileDiff) => { const loader = loadDiffFilesRef.current; if (!loader) throw new Error("Diff file contents are unavailable for this selection."); return loader(fileDiff); }, []); const localBranchRefs = useEnvironmentQuery( selectedTurnId === null && selectedGitScope === "branch" && activeThread && branchDiffPreview.data?.cwd ? vcsEnvironment.listRefs({ environmentId: activeThread.environmentId, input: { cwd: branchDiffPreview.data.cwd, includeMatchingRemoteRefs: true, refKind: "local", ...(baseRefQuery.trim().length > 0 ? { query: baseRefQuery.trim() } : {}), limit: 100, }, }) : null, ); const remoteBranchRefs = useEnvironmentQuery( selectedTurnId === null && selectedGitScope === "branch" && activeThread && branchDiffPreview.data?.cwd ? vcsEnvironment.listRefs({ environmentId: activeThread.environmentId, input: { cwd: branchDiffPreview.data.cwd, includeMatchingRemoteRefs: true, refKind: "remote", ...(baseRefQuery.trim().length > 0 ? { query: baseRefQuery.trim() } : {}), limit: 100, }, }) : null, ); const baseRefChoices = buildBaseRefChoices( localBranchRefs.data?.refs.filter((ref) => ref.name !== selectedGitSource?.headRef) ?? [], remoteBranchRefs.data?.refs ?? [], ); const matchingBaseRefChoices = filterBaseRefChoices(baseRefChoices, baseRefQuery); const valueForBaseRefChoice = (choice: (typeof baseRefChoices)[number]) => selectedBaseRef && selectedBaseRef === choice.remote?.name ? selectedBaseRef : (choice.local?.name ?? choice.remote?.name ?? choice.id); const baseRefItems = [AUTOMATIC_BASE_REF, ...baseRefChoices.map(valueForBaseRefChoice)]; const filteredBaseRefItems = [ ...(baseRefQuery.trim().length === 0 ? [AUTOMATIC_BASE_REF] : []), ...matchingBaseRefChoices.map(valueForBaseRefChoice), ]; const gitDiff = selectedGitSource?.diff; const selectedPatch = selectedTurn ? activeCheckpointDiff.data?.diff : gitDiff; const isSelectedPatchTruncated = !selectedTurn && selectedGitSource?.truncated === true; const isLoadingSelectedPatch = selectedTurn ? activeCheckpointDiff.isPending : branchDiffPreview.isPending; const selectedPatchError = selectedTurn ? activeCheckpointDiff.error : branchDiffPreview.error; const hasResolvedPatch = typeof selectedPatch === "string"; const hasNoNetChanges = hasResolvedPatch && selectedPatch.trim().length === 0; const renderablePatch = useMemo( () => getRenderablePatch(selectedPatch, `diff-panel:${resolvedTheme}`, { compactPartialHunkOffsets: selectedTurnId === null, }), [resolvedTheme, selectedPatch, selectedTurnId], ); const renderableFiles = useMemo(() => { if (!renderablePatch || renderablePatch.kind !== "files") { return []; } return renderablePatch.files.toSorted((left, right) => resolveFileDiffPath(left).localeCompare(resolveFileDiffPath(right), undefined, { numeric: true, sensitivity: "base", }), ); }, [renderablePatch]); const renderableFileEntries = useMemo( () => renderableFiles.map((fileDiff) => ({ fileDiff, fileKey: buildFileDiffIdentityKey(fileDiff), fileVersion: buildFileDiffContentVersion(fileDiff), })), [renderableFiles], ); const codeViewFiles = useMemo( () => renderableFileEntries.map(({ fileDiff, fileKey, fileVersion }) => { return { fileDiff, filePath: resolveFileDiffPath(fileDiff), fileKey, fileVersion, collapsed: collapsedDiffFileKeys.has(fileKey), }; }), [collapsedDiffFileKeys, renderableFileEntries], ); const diffFileKeys = useMemo(() => codeViewFiles.map((file) => file.fileKey), [codeViewFiles]); const allDiffFilesCollapsed = areAllDiffFilesCollapsed(diffFileKeys, collapsedDiffFileKeys); const diffLineStat = useMemo(() => getDiffLineStat(renderableFiles), [renderableFiles]); const fileTreeEntries = useMemo(() => diffFileTreeEntries(renderableFiles), [renderableFiles]); const selectedDiffFileKey = selectedFilePath ? (codeViewFiles.find((candidate) => candidate.filePath === selectedFilePath)?.fileKey ?? null) : null; useEffect(() => { if (!selectedDiffFileKey || !codeView?.getInstance()) return; codeView.scrollTo({ type: "item", id: selectedDiffFileKey, align: "start" }); }, [codeView, codeViewMountKey, selectedDiffFileKey, selectedFileRevealRequestId]); const treeRevealScope = useMemo( () => ({ collapseScopeKey, diffSelection }), [collapseScopeKey, diffSelection], ); const requestTreeReveal = useCodeViewFileReveal(codeView, treeRevealScope); const revealDiffFile = useCallback( (filePath: string) => { const file = codeViewFiles.find((candidate) => candidate.filePath === filePath); if (!file) return; if (file.collapsed) { setCollapsedDiffFiles((current) => { const next = new Set(current.scopeKey === collapseScopeKey ? current.fileKeys : []); next.delete(file.fileKey); return { scopeKey: collapseScopeKey, fileKeys: next }; }); } requestTreeReveal(file.fileKey); }, [codeViewFiles, collapseScopeKey, requestTreeReveal], ); const openDiffFile = useCallback( (filePath: string) => { openDiffFilePrimaryAction({ threadRef: routeThreadRef, filePath, activeCwd, repositoryRoot: activeRepositoryRoot, openInEditor: (targetPath) => { void (async () => { const result = await openInPreferredEditor(targetPath); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { console.warn("Failed to open diff file in editor.", { operation: "open-diff-file", ...(routeThreadRef ? { environmentId: routeThreadRef.environmentId, threadId: routeThreadRef.threadId, } : {}), ...safeErrorLogAttributes(squashAtomCommandFailure(result)), }); } })(); }, }); },Lines 1–500 of 1063. Load more
import { RefreshIcon } from "~/components/ui/refresh-icon"; import { useAtomValue } from "@effect/atom-react"; import type { FileDiffContentsLoader } from "@pierre/diffs"; import { useParams } from "@tanstack/react-router"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; import type { ScopedThreadRef, TurnId } from "@t3tools/contracts"; import { ArrowRightIcon, CheckIcon, ChevronDownIcon, ChevronRightIcon, ChevronsDownUpIcon, ChevronsUpDownIcon, Columns2Icon, FolderTreeIcon, PilcrowIcon, Rows3Icon, SearchIcon, TextWrapIcon, } from "lucide-react"; import * as Schema from "effect/Schema"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCodeViewFileReveal } from "./diffs/useCodeViewFileReveal"; import { useOpenInPreferredEditor } from "../editorPreferences"; import { type DraftId } from "../composerDraftStore"; import { openDiffFilePrimaryAction } from "../diffFileActions"; import { useCheckpointDiff } from "~/lib/checkpointDiffState"; import { cn } from "~/lib/utils"; import { selectThreadDiffPanelSelection, useDiffPanelStore } from "../diffPanelStore"; import { useLocalStorage } from "../hooks/useLocalStorage"; import { useTheme } from "../hooks/useTheme"; import { buildFileDiffContentVersion, buildFileDiffIdentityKey, getDiffCollapseIconClassName, getDiffLineStat, getRenderablePatch, resolveDiffThemeName, resolveFileDiffPath, } from "../lib/diffRendering"; import { PREFERRED_HIGHLIGHTER } from "../lib/syntaxHighlighting"; import { areAllDiffFilesCollapsed, toggleAllDiffFiles } from "../lib/diffCollapse"; import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; import { useWorkspaceMutationRefresh } from "../hooks/useWorkspaceMutationRefresh"; import { useProject, useThread } from "../state/entities"; import { resolveThreadRouteRef } from "../threadRoutes"; import { useClientSettings, useUpdateClientSettings } from "../hooks/useSettings"; import { formatShortTimestamp } from "../timestampFormat"; import { DiffFilePathCopyButton } from "./DiffFilePathCopyButton"; import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell"; import { DiffStatLabel } from "./chat/DiffStatLabel"; import { AnnotatableCodeView, type AnnotatableCodeViewHandle } from "./diffs/AnnotatableCodeView"; import { DiffFileTree } from "./diffs/DiffFileTree"; import { diffFileTreeEntries } from "./diffs/diffFileTree.logic"; import { Button } from "./ui/button"; import { ToggleGroup, Toggle } from "./ui/toggle-group"; import { Switch } from "./ui/switch"; import { Combobox, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, ComboboxPopup, ComboboxTrigger, } from "./ui/combobox"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "./ui/menu"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { useEnvironmentQuery } from "../state/query"; import { useAtomCommand } from "../state/use-atom-command"; import { serverEnvironment } from "../state/server"; import { reviewEnvironment } from "../state/review"; import { vcsEnvironment } from "../state/vcs"; import { buildBaseRefChoices, filterBaseRefChoices } from "../lib/baseRefChoices"; import { createGitDiffFileContentsLoader } from "../lib/diffFileContents"; type DiffThemeType = "light" | "dark"; const AUTOMATIC_BASE_REF = "__automatic_base_ref__"; const DIFF_FILE_TREE_STORAGE_KEY = "t3code.diffFileTreeOpen"; interface CollapsedDiffFilesState { readonly scopeKey: string | null; readonly fileKeys: ReadonlySet<string>; } const EMPTY_COLLAPSED_DIFF_FILE_KEYS: ReadonlySet<string> = new Set(); interface DiffPanelProps { mode?: DiffPanelMode; composerDraftTarget: ScopedThreadRef | DraftId; initialGitScope: "branch" | "unstaged"; workspaceMutationId: string | null; } export default function DiffPanel({ mode = "inline", composerDraftTarget, initialGitScope: initialGitScopeProp, workspaceMutationId, }: DiffPanelProps) { const { resolvedTheme } = useTheme(); const settings = useClientSettings(); const [initialGitScope] = useState(initialGitScopeProp); const diffLayout = settings.diffLayout; const updateClientSettings = useUpdateClientSettings(); const [wordWrap, setWordWrap] = useState(settings.wordWrap); const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace); const [fileTreeOpen, setFileTreeOpen] = useLocalStorage( DIFF_FILE_TREE_STORAGE_KEY, false, Schema.Boolean, ); const [baseRefQuery, setBaseRefQuery] = useState(""); const [collapsedDiffFiles, setCollapsedDiffFiles] = useState<CollapsedDiffFilesState>(() => ({ scopeKey: null, fileKeys: EMPTY_COLLAPSED_DIFF_FILE_KEYS, })); const [codeViewRevision, setCodeViewRevision] = useState(0); const [codeView, setCodeView] = useState<AnnotatableCodeViewHandle | null>(null); const routeThreadRef = useParams({ strict: false, select: (params) => resolveThreadRouteRef(params), }); const activeThreadId = routeThreadRef?.threadId ?? null; const activeThread = useThread(routeThreadRef); const activeProjectId = activeThread?.projectId ?? null; const activeProject = useProject( activeThread && activeProjectId ? { environmentId: activeThread.environmentId, projectId: activeProjectId, } : null, ); const activeCwd = activeThread?.worktreePath ?? activeProject?.workspaceRoot; const activeRepositoryRoot = activeThread?.worktreePath ? undefined : activeProject?.repositoryIdentity?.rootPath; const serverConfig = useAtomValue( serverEnvironment.configValueAtom(activeThread?.environmentId ?? null), ); const openInPreferredEditor = useOpenInPreferredEditor( activeThread?.environmentId ?? null, serverConfig?.availableEditors ?? [], ); const getDiffFileContents = useAtomCommand(reviewEnvironment.diffFileContents); const gitStatusQuery = useEnvironmentQuery( activeThread !== null && activeThread !== undefined && activeCwd != null ? vcsEnvironment.status({ environmentId: activeThread.environmentId, input: { cwd: activeCwd }, }) : null, ); const diffSelection = useDiffPanelStore((state) => selectThreadDiffPanelSelection( state.byThreadKey, routeThreadRef, initialGitScope === "unstaged", ), ); const isGitRepo = gitStatusQuery.data?.isRepo ?? true; const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } = useTurnDiffSummaries(activeThread); const orderedTurnDiffSummaries = useMemo( () => [...turnDiffSummaries].toSorted((left, right) => { const leftTurnCount = left.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[left.turnId] ?? 0; const rightTurnCount = right.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[right.turnId] ?? 0; if (leftTurnCount !== rightTurnCount) { return rightTurnCount - leftTurnCount; } return right.completedAt.localeCompare(left.completedAt); }), [inferredCheckpointTurnCountByTurnId, turnDiffSummaries], ); useEffect(() => { if (!routeThreadRef || diffSelection.kind !== "turn") return; useDiffPanelStore.getState().reconcileTurnSelection( routeThreadRef, orderedTurnDiffSummaries.map((summary) => summary.turnId), ); }, [diffSelection, orderedTurnDiffSummaries, routeThreadRef]); const selectedTurnId = diffSelection.kind === "turn" ? diffSelection.turnId : null; const selectedGitScope = diffSelection.kind === "unstaged" ? "unstaged" : "branch"; const selectedBaseRef = diffSelection.kind === "branch" ? diffSelection.baseRef : null; const selectedFilePath = diffSelection.kind === "turn" ? diffSelection.filePath : null; const selectedFileRevealRequestId = diffSelection.kind === "turn" ? diffSelection.revealRequestId : 0; const selectedTurn = selectedTurnId === null ? undefined : (orderedTurnDiffSummaries.find((summary) => summary.turnId === selectedTurnId) ?? orderedTurnDiffSummaries[0]); const selectedCheckpointTurnCount = selectedTurn && (selectedTurn.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[selectedTurn.turnId]); const latestTurn = orderedTurnDiffSummaries[0]; const selectedScopeLabel = selectedTurnId === null ? selectedGitScope === "unstaged" ? "Working tree" : "Branch changes" : selectedTurn?.turnId === latestTurn?.turnId ? "Latest turn" : `Turn ${selectedCheckpointTurnCount ?? "?"}`; const reviewSectionId = selectedTurn ? `turn:${selectedTurn.turnId}` : selectedGitScope; const collapseScopeKey = routeThreadRef ? `${routeThreadRef.environmentId}:${routeThreadRef.threadId}:${reviewSectionId}` : null; const codeViewMountKey = `${collapseScopeKey ?? reviewSectionId}:${codeViewRevision}`; const collapsedDiffFileKeys = collapsedDiffFiles.scopeKey === collapseScopeKey ? collapsedDiffFiles.fileKeys : EMPTY_COLLAPSED_DIFF_FILE_KEYS; const reviewSectionTitle = selectedTurn ? `Turn ${selectedCheckpointTurnCount ?? "?"}` : selectedGitScope === "unstaged" ? "Working tree" : "Branch changes"; const selectedCheckpointRange = useMemo( () => typeof selectedCheckpointTurnCount === "number" ? { fromTurnCount: Math.max(0, selectedCheckpointTurnCount - 1), toTurnCount: selectedCheckpointTurnCount, } : null, [selectedCheckpointTurnCount], ); const activeCheckpointDiff = useCheckpointDiff( { environmentId: activeThread?.environmentId ?? null, threadId: activeThreadId, fromTurnCount: selectedCheckpointRange?.fromTurnCount ?? null, toTurnCount: selectedCheckpointRange?.toTurnCount ?? null, ignoreWhitespace: diffIgnoreWhitespace, cacheScope: selectedTurn ? `turn:${selectedTurn.turnId}` : null, }, { enabled: isGitRepo && selectedTurn !== undefined }, ); const primaryBranchDiffPreview = useEnvironmentQuery( selectedTurnId === null && activeThread && activeCwd ? reviewEnvironment.diffPreview({ environmentId: activeThread.environmentId, input: { cwd: activeCwd, ...(selectedBaseRef ? { baseRef: selectedBaseRef } : {}), ignoreWhitespace: diffIgnoreWhitespace, }, }) : null, ); const shouldRetryBranchDiffAtEnvironmentCwd = selectedTurnId === null && primaryBranchDiffPreview.error?.includes("configured workspace root") === true && serverConfig?.cwd !== undefined && serverConfig.cwd !== activeCwd; const fallbackBranchDiffPreview = useEnvironmentQuery( shouldRetryBranchDiffAtEnvironmentCwd && activeThread && serverConfig ? reviewEnvironment.diffPreview({ environmentId: activeThread.environmentId, input: { cwd: serverConfig.cwd, ...(selectedBaseRef ? { baseRef: selectedBaseRef } : {}), ignoreWhitespace: diffIgnoreWhitespace, }, }) : null, ); const branchDiffPreview = shouldRetryBranchDiffAtEnvironmentCwd ? fallbackBranchDiffPreview : primaryBranchDiffPreview; const refreshBranchDiffPreview = branchDiffPreview.refresh; const canRefreshGitDiff = isGitRepo && selectedTurnId === null && activeThread != null && activeCwd != null; const activeThreadRefreshKey = routeThreadRef ? `${routeThreadRef.environmentId}:${routeThreadRef.threadId}` : null; useEffect(() => { if (!canRefreshGitDiff) return; const refreshOnFocus = () => refreshBranchDiffPreview(); window.addEventListener("focus", refreshOnFocus); return () => window.removeEventListener("focus", refreshOnFocus); }, [canRefreshGitDiff, refreshBranchDiffPreview]); useWorkspaceMutationRefresh({ enabled: canRefreshGitDiff, mutationId: workspaceMutationId, refresh: refreshBranchDiffPreview, resourceKey: `diff:${activeThreadRefreshKey ?? ""}`, }); const selectedGitSource = branchDiffPreview.data?.sources.find( (source) => source.kind === (selectedGitScope === "unstaged" ? "working-tree" : "branch-range"), ); const currentLoadDiffFiles = useMemo<FileDiffContentsLoader | undefined>(() => { const preview = branchDiffPreview.data; if (selectedTurnId !== null || !activeThread || !preview || !selectedGitSource) { return undefined; } return createGitDiffFileContentsLoader(getDiffFileContents, { environmentId: activeThread.environmentId, cwd: preview.cwd, sourceKind: selectedGitSource.kind, baseRef: selectedGitSource.baseRef, headRef: selectedGitSource.headRef, cacheKey: selectedGitSource.diffHash, }); }, [ activeThread, branchDiffPreview.data, getDiffFileContents, selectedGitSource, selectedTurnId, ]); const loadDiffFilesRef = useRef(currentLoadDiffFiles); loadDiffFilesRef.current = currentLoadDiffFiles; const loadDiffFiles = useCallback<FileDiffContentsLoader>(async (fileDiff) => { const loader = loadDiffFilesRef.current; if (!loader) throw new Error("Diff file contents are unavailable for this selection."); return loader(fileDiff); }, []); const localBranchRefs = useEnvironmentQuery( selectedTurnId === null && selectedGitScope === "branch" && activeThread && branchDiffPreview.data?.cwd ? vcsEnvironment.listRefs({ environmentId: activeThread.environmentId, input: { cwd: branchDiffPreview.data.cwd, includeMatchingRemoteRefs: true, refKind: "local", ...(baseRefQuery.trim().length > 0 ? { query: baseRefQuery.trim() } : {}), limit: 100, }, }) : null, ); const remoteBranchRefs = useEnvironmentQuery( selectedTurnId === null && selectedGitScope === "branch" && activeThread && branchDiffPreview.data?.cwd ? vcsEnvironment.listRefs({ environmentId: activeThread.environmentId, input: { cwd: branchDiffPreview.data.cwd, includeMatchingRemoteRefs: true, refKind: "remote", ...(baseRefQuery.trim().length > 0 ? { query: baseRefQuery.trim() } : {}), limit: 100, }, }) : null, ); const baseRefChoices = buildBaseRefChoices( localBranchRefs.data?.refs.filter((ref) => ref.name !== selectedGitSource?.headRef) ?? [], remoteBranchRefs.data?.refs ?? [], ); const matchingBaseRefChoices = filterBaseRefChoices(baseRefChoices, baseRefQuery); const valueForBaseRefChoice = (choice: (typeof baseRefChoices)[number]) => selectedBaseRef && selectedBaseRef === choice.remote?.name ? selectedBaseRef : (choice.local?.name ?? choice.remote?.name ?? choice.id); const baseRefItems = [AUTOMATIC_BASE_REF, ...baseRefChoices.map(valueForBaseRefChoice)]; const filteredBaseRefItems = [ ...(baseRefQuery.trim().length === 0 ? [AUTOMATIC_BASE_REF] : []), ...matchingBaseRefChoices.map(valueForBaseRefChoice), ]; const gitDiff = selectedGitSource?.diff; const selectedPatch = selectedTurn ? activeCheckpointDiff.data?.diff : gitDiff; const isSelectedPatchTruncated = !selectedTurn && selectedGitSource?.truncated === true; const isLoadingSelectedPatch = selectedTurn ? activeCheckpointDiff.isPending : branchDiffPreview.isPending; const selectedPatchError = selectedTurn ? activeCheckpointDiff.error : branchDiffPreview.error; const hasResolvedPatch = typeof selectedPatch === "string"; const hasNoNetChanges = hasResolvedPatch && selectedPatch.trim().length === 0; const renderablePatch = useMemo( () => getRenderablePatch(selectedPatch, `diff-panel:${resolvedTheme}`, { compactPartialHunkOffsets: selectedTurnId === null, }), [resolvedTheme, selectedPatch, selectedTurnId], ); const renderableFiles = useMemo(() => { if (!renderablePatch || renderablePatch.kind !== "files") { return []; } return renderablePatch.files.toSorted((left, right) => resolveFileDiffPath(left).localeCompare(resolveFileDiffPath(right), undefined, { numeric: true, sensitivity: "base", }), ); }, [renderablePatch]); const renderableFileEntries = useMemo( () => renderableFiles.map((fileDiff) => ({ fileDiff, fileKey: buildFileDiffIdentityKey(fileDiff), fileVersion: buildFileDiffContentVersion(fileDiff), })), [renderableFiles], ); const codeViewFiles = useMemo( () => renderableFileEntries.map(({ fileDiff, fileKey, fileVersion }) => { return { fileDiff, filePath: resolveFileDiffPath(fileDiff), fileKey, fileVersion, collapsed: collapsedDiffFileKeys.has(fileKey), }; }), [collapsedDiffFileKeys, renderableFileEntries], ); const diffFileKeys = useMemo(() => codeViewFiles.map((file) => file.fileKey), [codeViewFiles]); const allDiffFilesCollapsed = areAllDiffFilesCollapsed(diffFileKeys, collapsedDiffFileKeys); const diffLineStat = useMemo(() => getDiffLineStat(renderableFiles), [renderableFiles]); const fileTreeEntries = useMemo(() => diffFileTreeEntries(renderableFiles), [renderableFiles]); const selectedDiffFileKey = selectedFilePath ? (codeViewFiles.find((candidate) => candidate.filePath === selectedFilePath)?.fileKey ?? null) : null; useEffect(() => { if (!selectedDiffFileKey || !codeView?.getInstance()) return; codeView.scrollTo({ type: "item", id: selectedDiffFileKey, align: "start" }); }, [codeView, codeViewMountKey, selectedDiffFileKey, selectedFileRevealRequestId]); const treeRevealScope = useMemo( () => ({ collapseScopeKey, diffSelection }), [collapseScopeKey, diffSelection], ); const requestTreeReveal = useCodeViewFileReveal(codeView, treeRevealScope); const revealDiffFile = useCallback( (filePath: string) => { const file = codeViewFiles.find((candidate) => candidate.filePath === filePath); if (!file) return; if (file.collapsed) { setCollapsedDiffFiles((current) => { const next = new Set(current.scopeKey === collapseScopeKey ? current.fileKeys : []); next.delete(file.fileKey); return { scopeKey: collapseScopeKey, fileKeys: next }; }); } requestTreeReveal(file.fileKey); }, [codeViewFiles, collapseScopeKey, requestTreeReveal], ); const openDiffFile = useCallback( (filePath: string) => { openDiffFilePrimaryAction({ threadRef: routeThreadRef, filePath, activeCwd, repositoryRoot: activeRepositoryRoot, openInEditor: (targetPath) => { void (async () => { const result = await openInPreferredEditor(targetPath); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { console.warn("Failed to open diff file in editor.", { operation: "open-diff-file", ...(routeThreadRef ? { environmentId: routeThreadRef.environmentId, threadId: routeThreadRef.threadId, } : {}), ...safeErrorLogAttributes(squashAtomCommandFailure(result)), }); } })(); }, }); },