import { RefreshIcon } from "~/components/ui/refresh-icon"; import { useAtomValue } from "@effect/atom-react"; import type { FileDiffContentsLoader, FileDiffMetadata } from "@pierre/diffs"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; import { resolveCheckpointFilePath } from "@t3tools/client-runtime/state/thread-checkpoints"; import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks"; import type { ReviewBranchCommitsResult, ScopedThreadRef, RunId } from "@t3tools/contracts"; import { ArrowRightIcon, CheckIcon, ChevronDownIcon, Columns2Icon, FlaskConicalOffIcon, FolderTreeIcon, PilcrowIcon, Rows3Icon, TextWrapIcon, ChevronRightIcon, ChevronsDownUpIcon, ChevronsUpDownIcon, } from "lucide-react"; import * as Schema from "effect/Schema"; import { type MouseEvent, useEffect, useRef, useState } from "react"; import { useCodeViewFileReveal } from "./diffs/useCodeViewFileReveal"; import { useCodeViewScrollRestore } from "./diffs/useCodeViewScrollRestore"; import { useFilesystemReadAccess } from "~/state/filesystem"; import { useOpenInPreferredEditor } from "../editorPreferences"; import { useFileContextMenuHandler } from "../fileContextMenu"; import { type DraftId } from "../composerDraftStore"; import { openDiffFilePrimaryAction } from "../diffFileActions"; import { toolCallDiff } from "../lib/toolCallDiff"; import { useCheckpointDiff, useTurnItemDetail } from "~/state/queries"; import { revisionedQuery } from "~/state/revisionedQuery"; import { cn } from "~/lib/utils"; import { resolveToolCallDiffTarget, selectThreadBranchBaseRef, selectThreadDiffPanelSelection, useDiffPanelSessionStore, useDiffPanelStore, } from "../diffPanelStore"; import { useLocalStorage } from "../hooks/useLocalStorage"; import { isTestChangedFile } from "@t3tools/shared/changedFileKind"; import { useHideTestFiles } from "./diffs/useHideTestFiles"; import { useRenderedAppearance } from "../hooks/useTheme"; import { useStableCallback } from "../hooks/useStableCallback"; import { buildFileDiffContentVersion, buildFileDiffIdentityKey, getDiffLineStat, getRenderablePatch, resolveDiffThemeName, resolveFileDiffPath, } from "../lib/diffRendering"; import { PREFERRED_HIGHLIGHTER } from "../lib/syntaxHighlighting"; import { areAllDiffFilesCollapsed, toggleAllDiffFiles } from "../lib/diffCollapse"; import { LIVE_REFRESH_MIN_INTERVAL_MS, useLiveRefresh } from "../hooks/useLiveRefresh"; import { useProject, useThreadCheckpointSummaries, useQuietThreadShell, useQuietThreadVisibleTurnItems, } from "../state/entities"; import { useClientSettings, updateClientSettings } from "../hooks/useSettings"; import { formatRelativeTimeLabel, 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 { FileExplorerPane } from "./files/FileExplorerPane"; 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, ComboboxSearchInput, ComboboxItem, ComboboxList, ComboboxPopup, ComboboxTrigger, } from "./ui/combobox"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, 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"; import { observeIntersection } from "~/lib/observeIntersection"; import { useThreadRepoWorktreeTarget } from "../hooks/useThreadRepoWorktreeTarget"; import { useRightPanelStore } from "../rightPanelStore"; import { RepoWorktreeTargetMenu } from "./RepoWorktreeTargetMenu"; import { useReviewFilePatches } from "./diffs/useReviewFilePatches"; import { DiffFileLoadingBoundary } from "./diffs/DiffFileLoadingBoundary"; import { DiffFileStatus } from "./diffs/DiffFileStatus"; import { useThreadRouteRef } from "../hooks/useThreadRouteTarget"; import { projectCommitUrl } from "@t3tools/client-runtime/change-request-links"; import { useOpenLink } from "../browser/useOpenLink"; type DiffThemeType = "light" | "dark"; const AUTOMATIC_BASE_REF = "__automatic_base_ref__"; const DIFF_FILE_TREE_STORAGE_KEY = "t3code.diffFileTreeOpen"; const fileEntryCache = new WeakMap< FileDiffMetadata, { fileDiff: FileDiffMetadata; fileKey: string; fileVersion: number } >(); function getCachedFileEntry(fileDiff: FileDiffMetadata) { const cached = fileEntryCache.get(fileDiff); if (cached) return cached; const entry = { fileDiff, fileKey: buildFileDiffIdentityKey(fileDiff), fileVersion: buildFileDiffContentVersion(fileDiff), }; fileEntryCache.set(fileDiff, entry); return entry; } const EMPTY_COLLAPSED_DIFF_FILE_KEYS: ReadonlySet = new Set(); function DiffFileCollapseToggle({ filePath, fileKey, collapsed, onToggle, load, }: { filePath: string; fileKey: string; collapsed: boolean; load?: () => void; onToggle: (fileKey: string) => void; }) { return ( { if (!node || !load) return; return observeIntersection( node, (entry) => { if (entry.isIntersecting) load(); }, { rootMargin: "600px" }, ); }} onClick={(event) => { event.stopPropagation(); onToggle(fileKey); }} /> } > {collapsed ? ( ) : ( )} {collapsed ? "Expand diff" : "Collapse diff"} ); } function DiffFileHeaderSuffix({ filePath, hasStat, error, truncated, onRetry, }: { filePath: string; hasStat: boolean; error: boolean; truncated: boolean; onRetry: (path: string) => void; }) { return ( <> {hasStat ? ( onRetry(filePath)} /> ) : null} ); } interface DiffPanelProps { mode?: DiffPanelMode; composerDraftTarget: ScopedThreadRef | DraftId; workspaceMutationId: string | null; visible: boolean; } export default function DiffPanel({ mode = "inline", composerDraftTarget, workspaceMutationId, visible, }: DiffPanelProps) { const resolvedTheme = useRenderedAppearance(); const settings = useClientSettings((settings) => ({ diffFilesCollapsed: settings.diffFilesCollapsed, diffIgnoreWhitespace: settings.diffIgnoreWhitespace, diffLayout: settings.diffLayout, timestampFormat: settings.timestampFormat, wordWrap: settings.wordWrap, })); const diffLayout = settings.diffLayout; const [wordWrap, setWordWrap] = useState(settings.wordWrap); const diffIgnoreWhitespace = useDiffPanelSessionStore((state) => state.ignoreWhitespace) ?? settings.diffIgnoreWhitespace; const [fileTreeOpen, setFileTreeOpen] = useLocalStorage( DIFF_FILE_TREE_STORAGE_KEY, false, Schema.Boolean, ); const [hideTestFiles, setHideTestFiles] = useHideTestFiles(); const [baseRefQuery, setBaseRefQuery] = useState(""); // The open commit menu keeps the list it opened with, so rows do not move under the pointer. const [openCommitMenu, setOpenCommitMenu] = useState(null); const [codeViewRevision, setCodeViewRevision] = useState(0); const routeThreadRef = useThreadRouteRef(); const activeThreadId = routeThreadRef?.threadId ?? null; const activeThread = useQuietThreadShell(routeThreadRef); const fileAccess = useFilesystemReadAccess(activeThread?.environmentId ?? null); const { canReadFiles } = fileAccess; const activeProjectId = activeThread?.projectId ?? null; const activeProject = useProject( activeThread && activeProjectId ? { environmentId: activeThread.environmentId, projectId: activeProjectId, } : null, ); const workspaceCwd = activeThread?.worktreePath ?? activeProject?.workspaceRoot; const repoTarget = useThreadRepoWorktreeTarget({ threadRef: routeThreadRef ?? null, workspaceCwd: workspaceCwd ?? null, }); const selectedRepoWorktree = repoTarget.repoWorktree; // Changes and Git status follow the chosen repository; turn diffs cover all of them. const activeCwd = selectedRepoWorktree?.worktreePath ?? workspaceCwd; const workspaceRepositoryRoot = activeThread?.worktreePath ? undefined : activeProject?.repositoryIdentity?.rootPath; const serverConfig = useAtomValue( serverEnvironment.configValueAtom(activeThread?.environmentId ?? null), ); const onFileContextMenu = useFileContextMenuHandler(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), ); const rememberedBaseRef = useDiffPanelStore((state) => selectThreadBranchBaseRef(state, routeThreadRef), ); const toolCallSelection = diffSelection.kind === "tool-call" ? diffSelection : null; const isGitSelection = diffSelection.kind === "branch" || diffSelection.kind === "unstaged" || diffSelection.kind === "commit"; const toolCallItems = useQuietThreadVisibleTurnItems(toolCallSelection ? routeThreadRef : null); const toolCallDetail = useTurnItemDetail( toolCallSelection && routeThreadRef ? { ...resolveToolCallDiffTarget(toolCallSelection, toolCallItems), environmentId: routeThreadRef.environmentId, } : null, ); const toolCallItem = toolCallDetail.data?.item; const isGitRepo = gitStatusQuery.data?.isRepo ?? true; const turnDiffSummaries = useThreadCheckpointSummaries(routeThreadRef); // A folder that is not a Git repository still has turn diffs from its repository worktrees. const hasRepositoryTurns = turnDiffSummaries.some( (summary) => (summary.repositories?.length ?? 0) > 0, ); const orderedTurnDiffSummaries = turnDiffSummaries.toSorted((left, right) => { if (left.checkpointTurnCount !== right.checkpointTurnCount) { return right.checkpointTurnCount - left.checkpointTurnCount; } return right.completedAt.localeCompare(left.completedAt); }); // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- rewrites the persisted diff store selection to the latest turn when the selected turn disappears useEffect(() => { if (!routeThreadRef || diffSelection.kind !== "turn") return; useDiffPanelStore.getState().reconcileTurnSelection( routeThreadRef, orderedTurnDiffSummaries.map((summary) => summary.runId), ); }, [diffSelection, orderedTurnDiffSummaries, routeThreadRef]); const selectedRunId = diffSelection.kind === "turn" ? diffSelection.turnId : null; const selectedGitScope = diffSelection.kind === "unstaged" || diffSelection.kind === "commit" ? diffSelection.kind : "branch"; const selectedCommitSha = diffSelection.kind === "commit" ? diffSelection.sha : null; const selectedBaseRef = diffSelection.kind === "branch" || diffSelection.kind === "commit" ? diffSelection.baseRef : null; const selectedFilePath = diffSelection.kind === "turn" ? diffSelection.filePath : null; const selectedFileRevealRequestId = diffSelection.kind === "turn" ? diffSelection.revealRequestId : 0; const selectedTurn = selectedRunId === null ? undefined : (orderedTurnDiffSummaries.find((summary) => summary.runId === selectedRunId) ?? orderedTurnDiffSummaries[0]); const selectedCheckpointTurnCount = selectedTurn?.checkpointTurnCount; const latestTurn = orderedTurnDiffSummaries[0]; const reviewSectionId = toolCallSelection ? `tool-call:${toolCallSelection.threadId}:${toolCallSelection.itemId}` : selectedTurn ? `turn:${selectedTurn.runId}` : selectedCommitSha !== null ? `commit:${selectedCommitSha}` : selectedRepoWorktree === null ? selectedGitScope : `${selectedGitScope}:${selectedRepoWorktree.worktreePath}`; const collapseScopeKey = routeThreadRef ? `${routeThreadRef.environmentId}:${routeThreadRef.threadId}:${reviewSectionId}` : null; const codeViewMountKey = `${collapseScopeKey ?? reviewSectionId}:${codeViewRevision}`; const selectedCheckpointRange = typeof selectedCheckpointTurnCount === "number" ? { fromTurnCount: Math.max(0, selectedCheckpointTurnCount - 1), toTurnCount: selectedCheckpointTurnCount, } : null; const activeCheckpointDiff = useCheckpointDiff( { environmentId: activeThread?.environmentId ?? null, threadId: activeThreadId, fromTurnCount: selectedCheckpointRange?.fromTurnCount ?? null, toTurnCount: selectedCheckpointRange?.toTurnCount ?? null, ignoreWhitespace: diffIgnoreWhitespace, cacheScope: selectedTurn ? `turn:${selectedTurn.runId}` : null, }, { enabled: (isGitRepo || hasRepositoryTurns) && selectedTurn !== undefined }, ); const branchDiffPreviewInput = canReadFiles && isGitSelection && activeThread && activeCwd ? { cwd: activeCwd, ...(selectedBaseRef ? { baseRef: selectedBaseRef } : {}), ...(selectedCommitSha !== null ? { commitSha: selectedCommitSha } : {}), ignoreWhitespace: diffIgnoreWhitespace, metadataOnly: true, ...(selectedCommitSha === null ? { sourceKind: selectedGitScope === "unstaged" ? ("working-tree" as const) : ("branch-range" as const), } : {}), } : undefined; const canRefreshGitDiff = isGitRepo && isGitSelection && activeThread != null && activeCwd != null; // Read under the workspace mutation id, so each mutation refetches the diff; the panel stays // mounted while hidden, so a mutation made then is already reflected when it shows. const branchDiffPreview = useEnvironmentQuery( branchDiffPreviewInput && activeThread ? revisionedQuery( reviewEnvironment.diffPreview({ environmentId: activeThread.environmentId, input: branchDiffPreviewInput, }), canRefreshGitDiff ? workspaceMutationId : undefined, ) : null, ); const branchCommitsQuery = useEnvironmentQuery( visible && isGitRepo && canReadFiles && activeThread && activeCwd ? reviewEnvironment.branchCommits({ environmentId: activeThread.environmentId, input: { cwd: activeCwd, ...(rememberedBaseRef ? { baseRef: rememberedBaseRef } : {}), }, }) : null, ); const branchCommits = openCommitMenu ?? branchCommitsQuery.data; const activeThreadRefreshKey = routeThreadRef ? `${routeThreadRef.environmentId}:${routeThreadRef.threadId}` : null; const selectedGitSource = branchDiffPreview.data?.sources.find( (source) => source.kind === (selectedGitScope === "unstaged" ? "working-tree" : selectedGitScope === "commit" ? "commit" : "branch-range"), ); const comparisonHeadRef = selectedGitSource?.headRef ?? gitStatusQuery.data?.refName ?? "HEAD"; const comparisonBaseRef = selectedGitSource?.baseRef ?? selectedBaseRef ?? "Automatic"; const selectedCommitSubject = selectedCommitSha !== null && selectedGitSource?.headRef === selectedCommitSha ? selectedGitSource.commitSubject : undefined; const selectedGitScopeLabel = selectedCommitSha !== null ? (selectedCommitSubject ?? `Commit ${selectedCommitSha.slice(0, 7)}`) : selectedGitScope === "unstaged" ? "Uncommitted" : "Changes"; const selectedScopeLabel = toolCallSelection ? "Tool call" : selectedRunId === null ? selectedCommitSha !== null ? (selectedCommitSubject ?? "Commit") : selectedGitScopeLabel : selectedTurn?.runId === latestTurn?.runId ? "Latest turn" : `Turn ${selectedCheckpointTurnCount ?? "?"}`; // Review comments carry this title, so it names the repository their paths belong to. const reviewSectionTitle = toolCallSelection ? "Tool call" : selectedTurn ? `Turn ${selectedCheckpointTurnCount ?? "?"}` : `${selectedGitScopeLabel}${ selectedRepoWorktree === null ? "" : ` in ${selectedRepoWorktree.repository} · ${selectedRepoWorktree.branch}` }`; const headerCommitSha = selectedRunId === null ? selectedCommitSha : null; const selectedCommitUrl = headerCommitSha !== null ? projectCommitUrl(activeProject?.repositoryIdentity, headerCommitSha) : null; const openLink = useOpenLink(routeThreadRef); // A SHA link goes to the code host; it must not route back into this pane's commit selection. const openCommitUrl = (event: MouseEvent, url: string) => { event.stopPropagation(); if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button === 1) return; event.preventDefault(); void openLink(url, { event }); }; const refreshPreviewQuery = branchDiffPreview.refresh; const refreshDiffFromUserAction = refreshPreviewQuery; const preview = branchDiffPreview.data; const gitDiff = selectedGitSource?.diff; const toolCallPreview = toolCallSelection && toolCallItem?.type === "file_change" ? toolCallDiff(toolCallItem) : null; const selectedPatch = toolCallSelection ? toolCallPreview?.patch : selectedTurn ? activeCheckpointDiff.data?.diff : gitDiff; const isSelectedPatchTruncated = toolCallPreview?.truncated === true || (isGitSelection && selectedGitSource?.truncated === true); const selectedPatchQuery = toolCallSelection ? toolCallDetail : selectedTurn ? activeCheckpointDiff : branchDiffPreview; const isLoadingSelectedPatch = selectedPatchQuery.isPending && (selectedPatchQuery.data === null || (isGitSelection && preview?.incomplete === true && selectedGitSource?.files?.length === 0)); const selectedPatchError = toolCallSelection ? toolCallDetail.error : selectedTurn ? activeCheckpointDiff.error : branchDiffPreview.error; const hasResolvedPatch = typeof selectedPatch === "string"; const hasNoNetChanges = hasResolvedPatch && selectedPatch.trim().length === 0 && !preview?.incomplete && (!selectedGitSource?.patchesDeferred || selectedGitSource.files?.length === 0); const lazySource = isGitSelection && (selectedGitSource?.truncated || selectedGitSource?.patchesDeferred) && selectedGitSource.files?.length ? selectedGitSource : null; const renderablePatch = getRenderablePatch( lazySource ? undefined : selectedPatch, `diff-panel:${resolvedTheme}`, { compactPartialHunkOffsets: isGitSelection, ...(toolCallSelection ? { ignoreWhitespace: diffIgnoreWhitespace } : {}), }, ); const recordedToolCallFileCount = toolCallSelection && toolCallItem?.type === "file_change" ? (toolCallItem.changes?.length ?? 0) : 0; const omittedToolCallFiles = renderablePatch?.kind === "files" ? Math.max(0, recordedToolCallFileCount - renderablePatch.files.length) : 0; const fileStats = new Map(lazySource?.files?.map((file) => [file.path, file])); const sourceLineStat = isGitSelection && selectedGitSource?.files ? selectedGitSource.files.reduce( (total, file) => ({ additions: total.additions + file.additions, deletions: total.deletions + file.deletions, }), { additions: 0, deletions: 0 }, ) : null; const currentLoadDiffFiles = isGitSelection && activeThread && preview && selectedGitSource && canReadFiles ? createGitDiffFileContentsLoader(getDiffFileContents, { environmentId: activeThread.environmentId, cwd: preview.cwd, sourceKind: selectedGitSource.kind, baseRef: selectedGitSource.baseRef, headRef: selectedGitSource.headRef, cacheKey: selectedGitSource.diffHash, }) : undefined; const loadDiffFiles: FileDiffContentsLoader = useStableCallback(async (fileDiff) => { if (!currentLoadDiffFiles) { throw new Error("Diff file contents are unavailable for this selection."); } return currentLoadDiffFiles(fileDiff); }); const canSelectBranchBase = canReadFiles && canRefreshGitDiff && selectedGitScope === "branch"; const branchRefsCwd = branchDiffPreview.data?.cwd ?? activeCwd; const localBranchRefs = useEnvironmentQuery( canSelectBranchBase && activeThread && branchRefsCwd ? vcsEnvironment.listRefs({ environmentId: activeThread.environmentId, input: { cwd: branchRefsCwd, includeMatchingRemoteRefs: true, refKind: "local", ...(baseRefQuery.trim().length > 0 ? { query: baseRefQuery.trim() } : {}), limit: 100, }, }) : null, ); const remoteBranchRefs = useEnvironmentQuery( canSelectBranchBase && activeThread && branchRefsCwd ? vcsEnvironment.listRefs({ environmentId: activeThread.environmentId, input: { cwd: branchRefsCwd, includeMatchingRemoteRefs: true, refKind: "remote", ...(baseRefQuery.trim().length > 0 ? { query: baseRefQuery.trim() } : {}), limit: 100, }, }) : null, ); const baseRefChoices = buildBaseRefChoices( localBranchRefs.data?.refs.filter((ref) => ref.name !== comparisonHeadRef) ?? [], 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 { isPending: areFilePatchesPending, fileStates, retry, requestFile, renderableFiles, remainingFileCount, loadNextFiles, } = useReviewFilePatches({ environmentId: activeThread?.environmentId, cwd: branchDiffPreview.data?.cwd, source: lazySource, baseRef: selectedCommitSha !== null ? selectedBaseRef : (lazySource?.baseRef ?? selectedBaseRef), ignoreWhitespace: diffIgnoreWhitespace, theme: resolvedTheme, previewInput: branchDiffPreviewInput, preview: renderablePatch, }); const refreshBranchDiffPreview = refreshPreviewQuery; const diffRefreshKey = `diff:${activeThreadRefreshKey ?? ""}`; // The panel stays mounted while hidden, and edits made outside T3 Code bump no mutation id, so // showing it revalidates the diff; that also stands in for focus events missed while hidden. // A git diff is too costly to repeat on a timer nobody asked for. A diff read within the floor, // such as a mutation's refetch while hidden, already counts as that revalidation. useLiveRefresh( branchDiffPreview.data !== null && !branchDiffPreview.isPending ? () => { if (Date.now() - branchDiffPreview.dataUpdatedAt < LIVE_REFRESH_MIN_INTERVAL_MS) return; refreshBranchDiffPreview(); } : null, { enabled: visible && canReadFiles && canRefreshGitDiff, key: diffRefreshKey, intervalMs: null, refreshOnShow: true, }, ); const isRefreshingDiff = branchDiffPreview.isPending || areFilePatchesPending; const pathOccurrences = new Map(); const renderableFileEntries = renderableFiles.map((fileDiff) => { const entry = getCachedFileEntry(fileDiff); if (!isGitSelection) return entry; // Metadata has no change type; keep each path's identity when its patch arrives. const pathKey = JSON.stringify([fileDiff.prevName ?? fileDiff.name, fileDiff.name]); const occurrence = pathOccurrences.get(pathKey) ?? 0; pathOccurrences.set(pathKey, occurrence + 1); return { ...entry, fileKey: `${pathKey}:${occurrence}` }; }); const diffFileKeys = renderableFileEntries.map((file) => file.fileKey); const testFileCount = renderableFiles.filter((file) => isTestChangedFile(resolveFileDiffPath(file)), ).length; // Hiding is display-only: lazy loading indexes into the full list. A requested file stays visible. const isFileShown = (file: FileDiffMetadata) => { const path = resolveFileDiffPath(file); return !hideTestFiles || path === selectedFilePath || !isTestChangedFile(path); }; const shownFileEntries = renderableFileEntries.filter(({ fileDiff }) => isFileShown(fileDiff)); const shownFileKeys = shownFileEntries.map((file) => file.fileKey); const defaultCollapsedDiffFileKeys = settings.diffFilesCollapsed ? new Set(diffFileKeys) : EMPTY_COLLAPSED_DIFF_FILE_KEYS; const storedCollapsedDiffFileKeys = useDiffPanelSessionStore((state) => collapseScopeKey ? state.collapsedFileKeysByScopeKey[collapseScopeKey] : undefined, ); const collapsedDiffFileKeys = storedCollapsedDiffFileKeys ?? defaultCollapsedDiffFileKeys; const updateCollapsedDiffFiles = ( update: (current: ReadonlySet) => ReadonlySet, ) => { if (!collapseScopeKey) return; useDiffPanelSessionStore .getState() .updateCollapsedFileKeys(collapseScopeKey, defaultCollapsedDiffFileKeys, update); }; const renderLoadingBoundary = () => remainingFileCount > 0 ? ( ) : null; const codeViewFiles = shownFileEntries.map(({ fileDiff, fileKey, fileVersion }) => ({ fileDiff, filePath: resolveFileDiffPath(fileDiff), fileKey, fileVersion, collapsed: collapsedDiffFileKeys.has(fileKey) || fileDiff.cacheKey?.endsWith(":pending") === true, })); const pendingFiles = renderableFiles.filter( (file) => file.cacheKey?.endsWith(":pending") && isFileShown(file), ); const allDiffFilesCollapsed = areAllDiffFilesCollapsed(shownFileKeys, collapsedDiffFileKeys); const diffLineStat = sourceLineStat ?? getDiffLineStat(renderableFiles); const fileTreeEntries = diffFileTreeEntries(renderableFiles.filter(isFileShown)); const codeViewReveal = useCodeViewFileReveal(); // Lazy files that sat above the saved position load again only when asked for. const codeViewScroll = useCodeViewScrollRestore( collapseScopeKey, !isRefreshingDiff && remainingFileCount === 0, remainingFileCount > 0 ? loadNextFiles : null, ); const attachCodeView = useStableCallback((handle: AnnotatableCodeViewHandle | null) => { codeViewScroll.attach(handle); codeViewReveal.attach(handle); }); const flushCodeView = useStableCallback(() => { codeViewScroll.flush(); codeViewReveal.flush(); }); const revealDiffFile = (filePath: string) => { const index = renderableFileEntries.findIndex( (candidate) => resolveFileDiffPath(candidate.fileDiff) === filePath, ); const file = renderableFileEntries[index]; if (!file) return false; codeViewScroll.discard(); updateCollapsedDiffFiles((current) => { const next = new Set(current); next.delete(file.fileKey); return next; }); if (lazySource) requestFile(filePath); codeViewReveal.request(file.fileKey); return true; }; // A file another view asked to reveal (a chat diff link): retried as the diff loads until it // holds the file, then not again until a new request, so it does not undo what the user did since. const externalRevealRef = useRef(null); // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- reveals an externally requested file in the diff viewer, retrying as the diff DOM loads useEffect(() => { if (!selectedFilePath) { externalRevealRef.current = null; return; } const key = JSON.stringify([collapseScopeKey, selectedFilePath, selectedFileRevealRequestId]); if (externalRevealRef.current === key) return; if (revealDiffFile(selectedFilePath)) { externalRevealRef.current = key; } }, [collapseScopeKey, selectedFilePath, selectedFileRevealRequestId, revealDiffFile]); // A turn diff names repository files by their prefix; a repository's // Changes are relative to its worktree. Either way the file opens there. const repositoryFileTarget = (filePath: string) => { if (selectedTurn) { const target = resolveCheckpointFilePath(selectedTurn.repositories, filePath); return target === null ? null : { filePath: target.relativePath, workspaceRoot: target.worktreePath }; } return selectedRepoWorktree === null ? null : { filePath, workspaceRoot: selectedRepoWorktree.worktreePath }; }; const openDiffFile = (filePath: string) => { const repositoryFile = repositoryFileTarget(filePath); if (repositoryFile !== null) { const absolutePath = resolvePathLinkTarget( repositoryFile.filePath, repositoryFile.workspaceRoot, ); if (routeThreadRef) { useRightPanelStore.getState().openFile(routeThreadRef, absolutePath); } else { void openInPreferredEditor(absolutePath); } return; } openDiffFilePrimaryAction({ threadRef: routeThreadRef, filePath, activeCwd: workspaceCwd, repositoryRoot: workspaceRepositoryRoot, 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)), }); } })(); }, }); }; const toggleDiffFileCollapsed = useStableCallback((fileKey: string) => { const file = renderableFileEntries.find((file) => file.fileKey === fileKey); if (lazySource && file) requestFile(resolveFileDiffPath(file.fileDiff)); const pending = file?.fileDiff.cacheKey?.endsWith(":pending") === true; updateCollapsedDiffFiles((current) => { const next = new Set(current); if (pending || next.has(fileKey)) { next.delete(fileKey); } else { next.add(fileKey); } return next; }); }); // Find can ask again before the unfolded file reaches the viewer, so this must never fold. const unfoldDiffFile = useStableCallback((fileKey: string) => { updateCollapsedDiffFiles((current) => { if (!current.has(fileKey)) return current; const next = new Set(current); next.delete(fileKey); return next; }); }); const toggleDiffFileCollapse = () => { codeViewScroll.discard(); setCodeViewRevision((current) => current + 1); updateCollapsedDiffFiles((current) => toggleAllDiffFiles(shownFileKeys, current)); }; const selectTurn = (runId: RunId) => { if (!routeThreadRef) return; useDiffPanelStore.getState().selectTurn(routeThreadRef, runId); }; const selectGitScope = (scope: "branch" | "unstaged") => { if (!routeThreadRef) return; useDiffPanelStore.getState().selectGitScope(routeThreadRef, scope); }; const selectBranchBaseRef = (baseRef: string | null) => { if (!routeThreadRef) return; useDiffPanelStore.getState().selectBranchBaseRef(routeThreadRef, baseRef); }; const selectedTurnValue = selectedTurn ? `turn:${selectedTurn.runId}` : ""; const selectedCommitValue = selectedCommitSha !== null ? `commit:${selectedCommitSha}` : ""; const selectedScopeValue = toolCallSelection ? "" : selectedRunId === null ? selectedCommitValue || selectedGitScope : selectedTurn?.runId === latestTurn?.runId ? "latest" : selectedTurnValue; const selectScopeValue = (value: string) => { if (value === "unstaged" || value === "branch") { selectGitScope(value); } else if (value === selectedCommitValue) { selectGitScope("branch"); } else if (value.startsWith("commit:")) { if (routeThreadRef) { useDiffPanelStore.getState().selectCommit(routeThreadRef, value.slice("commit:".length)); } } else if (value === "latest") { if (latestTurn) selectTurn(latestTurn.runId); } else { const turn = orderedTurnDiffSummaries.find((summary) => `turn:${summary.runId}` === value); if (turn) selectTurn(turn.runId); } }; const headerRow = ( <>
} className={cn("max-w-full", headerCommitSha !== null && "min-w-0 shrink")} aria-label={`Diff scope: ${selectedScopeLabel}`} data-workspace-focus-default > {selectedScopeLabel} {headerCommitSha !== null ? ( selectedCommitUrl ? ( openCommitUrl(event, selectedCommitUrl)} /> } > {headerCommitSha.slice(0, 7)} {selectedCommitUrl} ) : ( {headerCommitSha.slice(0, 7)} ) ) : null} Changes Uncommitted Latest turn Turn {orderedTurnDiffSummaries.map((summary) => { const turnCount = summary.checkpointTurnCount; return ( Turn {turnCount} {formatShortTimestamp(summary.completedAt, settings.timestampFormat)} ); })} { setOpenCommitMenu(open ? (branchCommitsQuery.data ?? null) : null); if (!open) return; // The menu keeps the list it opened with; the refreshed one shows next time. branchCommitsQuery.refresh(); }} > Commits {branchCommits?.commits.map((commit) => { const commitUrl = projectCommitUrl( activeProject?.repositoryIdentity, commit.sha, ); return (
} > {commit.subject} {formatRelativeTimeLabel(commit.committedAt)} {commit.subject} {commitUrl ? ( } onClick={(event) => openCommitUrl(event, commitUrl)} > {commit.sha.slice(0, 7)} ) : ( {commit.sha.slice(0, 7)} )}
); })}
{branchCommitsQuery.error ? ( {branchCommitsQuery.error} ) : !canReadFiles ? ( {fileAccess.isPending ? "Checking file access..." : (fileAccess.error ?? "This connection cannot read local diffs.")} ) : branchCommits === null ? ( Loading commits... ) : branchCommits.commits.length === 0 ? ( No commits on this branch. ) : branchCommits.truncated ? ( {`Showing latest ${branchCommits.commits.length} commits`} ) : null}
{canSelectBranchBase && (
} > {comparisonHeadRef} {`${comparisonHeadRef} → ${comparisonBaseRef}`} } > vs {`${comparisonHeadRef} → ${comparisonBaseRef}`} { if (!open) setBaseRefQuery(""); }} onValueChange={(value) => { if (!value) return; selectBranchBaseRef(value === AUTOMATIC_BASE_REF ? null : value); }} > } className="min-w-9 max-w-48 shrink-0 @max-[20rem]/diff-refs:shrink" aria-label={`Change comparison target. Currently ${comparisonBaseRef}`} > {comparisonBaseRef} setBaseRefQuery(event.target.value)} />
No matching refs. Automatic {baseRefChoices.map((choice) => { const item = valueForBaseRefChoice(choice); const hasBoth = choice.local !== null && choice.remote !== null; const useRemote = choice.remote?.name === item; return (
{choice.label} {hasBoth ? (
event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} > { const nextRef = checked ? choice.remote?.name : choice.local?.name; if (nextRef) selectBranchBaseRef(nextRef); }} />
) : choice.remote ? ( } /> Remote only ) : null}
); })}
)}
{codeViewFiles.length > 0 || (isGitSelection && selectedGitSource?.files?.length) ? ( ) : null} {canRefreshGitDiff && ( } > {isRefreshingDiff ? "Refreshing diff…" : "Refresh diff"} )} {diffFileKeys.length > 0 && ( } > {allDiffFilesCollapsed ? ( ) : ( )} {allDiffFilesCollapsed ? "Expand all files" : "Collapse all files"} )} { const next = value[0]; if (next === "stacked" || next === "split") { updateClientSettings({ diffLayout: next }); } }} > { setWordWrap(Boolean(pressed)); }} /> } > {wordWrap ? "Disable line wrapping" : "Enable line wrapping"} { useDiffPanelSessionStore.getState().setIgnoreWhitespace(Boolean(pressed)); }} /> } > {diffIgnoreWhitespace ? "Show whitespace changes" : "Hide whitespace changes"} {testFileCount > 0 && ( setHideTestFiles(Boolean(pressed))} /> } > {hideTestFiles ? `Show ${testFileCount} hidden test ${testFileCount === 1 ? "file" : "files"}` : "Hide test files"} )} {diffFileKeys.length > 0 && ( setFileTreeOpen(Boolean(pressed))} /> } > {fileTreeOpen ? "Hide file tree" : "Show file tree"} )}
); return ( {/* Its own row: beside the scope and comparison, a normal-width panel has no room left. */} {selectedRunId === null && !toolCallSelection && repoTarget.repoWorktrees.length > 0 ? (
) : null} {!activeThread ? (
Select a thread to inspect turn diffs.
) : !isGitRepo && !toolCallSelection && (selectedRunId === null || !hasRepositoryTurns) ? (
{selectedRunId === null && repoTarget.repoWorktrees.length > 0 ? "This folder is not a Git repository. Choose one of its repository worktrees above." : "Turn diffs are unavailable because this project is not a git repository."}
) : selectedRunId !== null && orderedTurnDiffSummaries.length === 0 ? (
No completed turns yet.
) : isGitSelection && !canReadFiles ? ( fileAccess.isPending ? ( ) : (
{fileAccess.error ?? "This connection cannot read local diffs."}
) ) : ( <>
{preview?.incomplete && (

{selectedPatchError ? "Showing tracked changes. Untracked changes could not be loaded. Refresh to retry." : "Showing tracked changes. Checking untracked files..."}

)} {isSelectedPatchTruncated && !lazySource && (

This preview exceeds the size limit. Changes shown are incomplete. {isGitSelection && selectedGitSource?.files && !preview?.incomplete ? " Totals include all changes." : ""}

)} {omittedToolCallFiles > 0 && (

This tool call changed {recordedToolCallFileCount} files. The recorded patch omits{" "} {omittedToolCallFiles} of them.

)} {selectedPatchError && !renderablePatch && (

{selectedPatchError}

)} {!renderablePatch && toolCallPreview && toolCallPreview.content.length > 0 ? (

The provider recorded file content without a diff.

{toolCallPreview.content.map(({ label, text }) => (

{label}

                      {text === "" ? "Empty content." : text}
                    
))}
) : !renderablePatch && !lazySource ? ( isLoadingSelectedPatch ? ( ) : (

{hasNoNetChanges ? "No net changes in this selection." : "No patch available for this selection."}

) ) : lazySource || renderablePatch?.kind === "files" ? (
{ const composedPath = event.nativeEvent.composedPath?.() ?? []; for (const node of composedPath) { if (!(node instanceof HTMLElement)) continue; // Header buttons own their clicks; bubbling a chevron click would toggle twice. if (node instanceof HTMLButtonElement || node instanceof HTMLAnchorElement) { return; } } const title = composedPath.find( (node): node is HTMLElement => node instanceof HTMLElement && node.hasAttribute("data-title"), ); const filePath = title?.textContent; if (filePath) { openDiffFile(filePath); return; } const header = composedPath.find( (node): node is HTMLElement => node instanceof HTMLElement && node.hasAttribute("data-diffs-header"), ); const headerFilePath = header?.querySelector("[data-title]")?.textContent; if (!headerFilePath) return; const file = codeViewFiles.find( (candidate) => candidate.filePath === headerFilePath, ); if (file) toggleDiffFileCollapsed(file.fileKey); }} onContextMenuCapture={(event) => { const composedPath = event.nativeEvent.composedPath?.() ?? []; const title = composedPath.find( (node): node is HTMLElement => node instanceof HTMLElement && node.hasAttribute("data-title"), ); const filePath = title?.textContent?.trim(); if (!filePath) return; event.preventDefault(); const repositoryFile = repositoryFileTarget(filePath); onFileContextMenu( repositoryFile === null ? { environmentId: activeThread?.environmentId ?? null, filePath, workspaceRoot: workspaceCwd, repositoryRoot: workspaceRepositoryRoot, } : { environmentId: activeThread?.environmentId ?? null, ...repositoryFile, repositoryRoot: undefined, }, event, ); }} > { const path = resolveFileDiffPath(fileDiff); const state = fileStates.get(path); return ( ); }} {...(lazySource ? { unsafeCSSExtra: "[data-additions-count], [data-deletions-count] { display: none; }", renderHeaderMetadata: (fileDiff: FileDiffMetadata) => { const stat = fileStats.get(resolveFileDiffPath(fileDiff)); return stat ? ( ) : null; }, } : {})} onRevealSearchMatch={unfoldDiffFile} renderHeaderPrefix={(fileDiff, fileKey, collapsed) => { return ( requestFile(resolveFileDiffPath(fileDiff)), } : {})} /> ); }} options={{ diffStyle: diffLayout === "split" ? "split" : "unified", lineDiffType: "none", overflow: wordWrap ? "wrap" : "scroll", theme: resolveDiffThemeName(resolvedTheme), preferredHighlighter: PREFERRED_HIGHLIGHTER, themeType: resolvedTheme as DiffThemeType, stickyHeaders: true, onPostRender: flushCodeView, ...(currentLoadDiffFiles ? { loadDiffFiles } : {}), }} />
{fileTreeOpen ? ( ) : null}
) : (

{renderablePatch?.kind === "raw" ? renderablePatch.reason : null}

                    {renderablePatch?.kind === "raw" ? renderablePatch.text : null}
                  
)}
)}
); }