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, PilcrowIcon, RefreshCwIcon, Rows3Icon, SearchIcon, TextWrapIcon,} from "lucide-react";import { useCallback, useEffect, useMemo, useRef, useState } from "react";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 { useTheme } from "../hooks/useTheme";import { buildFileDiffRenderKey, getDiffCollapseIconClassName, getDiffLineStat, getRenderablePatch, resolveDiffThemeName, resolveFileDiffPath, DIFF_SURFACE_THEME_UNSAFE_CSS,} from "../lib/diffRendering";import { areAllDiffFilesCollapsed, toggleAllDiffFiles } from "../lib/diffCollapse";import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";import { useProject, useThread } from "../state/entities";import { resolveThreadRouteRef } from "../threadRoutes";import { useClientSettings } from "../hooks/useSettings";import { formatShortTimestamp } from "../timestampFormat";import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell";import { DiffStatLabel } from "./chat/DiffStatLabel";import { AnnotatableCodeView, type AnnotatableCodeViewHandle } from "./diffs/AnnotatableCodeView";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"; type DiffRenderMode = "stacked" | "split";type DiffThemeType = "light" | "dark";const AUTOMATIC_BASE_REF = "__automatic_base_ref__"; interface CollapsedDiffFilesState { readonly scopeKey: string | null; readonly fileKeys: ReadonlySet<string>;} const EMPTY_COLLAPSED_DIFF_FILE_KEYS: ReadonlySet<string> = new Set(); const DIFF_PANEL_UNSAFE_CSS = `${DIFF_SURFACE_THEME_UNSAFE_CSS}:is( [data-line], [data-line-annotation], [data-merge-conflict], [data-merge-conflict-actions], [data-no-newline])[data-selected-line] { --diffs-line-bg: light-dark( color-mix( in lab, var(--code-background) 88%, color-mix(in srgb, var(--code-background) 50%, var(--diffs-modified-base)) ), color-mix( in lab, var(--code-background) 80%, color-mix(in srgb, var(--code-background) 70%, var(--diffs-modified-base)) ) ) !important;} :is([data-gutter-buffer], [data-column-number])[data-selected-line] { --diffs-line-bg: light-dark( color-mix( in lab, var(--code-background) 91%, color-mix(in srgb, var(--code-background) 35%, var(--diffs-modified-base)) ), color-mix( in lab, var(--code-background) 85%, color-mix(in srgb, var(--code-background) 60%, var(--diffs-modified-base)) ) ) !important;} [data-indicators="bars"] :is([data-column-number], [data-gutter-buffer="annotation"])[data-selected-line] { position: relative;} [data-indicators="bars"] :is([data-column-number], [data-gutter-buffer="annotation"])[data-selected-line]::before { position: absolute !important; inset-block: 0 !important; inset-inline-start: 0 !important; display: block !important; width: 4px !important; min-width: 4px !important; max-width: 4px !important; height: auto !important; padding: 0 !important; content: "" !important; background-color: var(--diffs-modified-base) !important; background-image: none !important;} [data-file-info] { background-color: var(--code-background) !important; border-block-color: transparent !important; color: var(--code-foreground) !important;} [data-diffs-header] { position: sticky !important; top: 0; z-index: 4; background-color: var(--code-background) !important; border-bottom-color: transparent !important; align-items: center !important; font-family: var(--font-sans) !important; font-size: 12px !important; line-height: 1 !important; min-height: 32px !important; padding-block: 6px !important; padding-inline: 8px 12px !important;} [data-diffs-header]:hover { background-color: color-mix(in srgb, var(--code-background) 97%, var(--code-foreground)) !important;} :is([data-separator="line-info"], [data-separator="line-info-basic"]) { height: 24px !important; margin-block: 0 !important; background-color: var(--code-background) !important;} :is([data-separator="line-info"], [data-separator="line-info-basic"]) [data-separator-wrapper] { padding-inline: 8px 12px !important; background-color: transparent !important;} :is([data-separator="line-info"], [data-separator="line-info-basic"]) [data-separator-content] { gap: 8px; padding-inline: 0 !important; background-color: transparent !important; color: color-mix(in srgb, var(--code-foreground) 52%, var(--code-background)) !important; font-family: var(--font-sans) !important; font-size: 11px !important; text-decoration: none !important;} :is([data-separator="line-info"], [data-separator="line-info-basic"]) [data-unmodified-lines] { display: flex !important; min-width: 0; flex: 1 1 auto; align-items: center; gap: 8px; cursor: pointer;} :is([data-separator="line-info"], [data-separator="line-info-basic"]) [data-unmodified-lines]::before,:is([data-separator="line-info"], [data-separator="line-info-basic"]) [data-unmodified-lines]::after { width: auto; height: 1px; flex: 1 1 auto; content: ""; background-color: color-mix(in srgb, var(--code-background) 92%, var(--code-foreground));} :is([data-separator="line-info"], [data-separator="line-info-basic"])[data-expand-index] [data-separator-wrapper] { grid-template-columns: 0 minmax(0, 1fr) !important;} :is([data-separator="line-info"], [data-separator="line-info-basic"])[data-expand-index] [data-separator-content] { grid-column: 2 !important;} :is([data-separator="line-info"], [data-separator="line-info-basic"]) [data-expand-button] { display: none !important;} :is([data-separator="line-info"], [data-separator="line-info-basic"]):has( [data-expand-button] ) [data-separator-content] { cursor: pointer;} :is([data-separator="line-info"], [data-separator="line-info-basic"]):has( [data-expand-button] ):hover [data-separator-content] { color: color-mix(in srgb, var(--code-foreground) 76%, var(--code-background)) !important;} :is([data-separator="line-info"], [data-separator="line-info-basic"]):has( [data-expand-button] ):hover [data-unmodified-lines]::before,:is([data-separator="line-info"], [data-separator="line-info-basic"]):has( [data-expand-button] ):hover [data-unmodified-lines]::after { background-color: color-mix(in srgb, var(--code-background) 84%, var(--code-foreground));} [data-diffs-header] [data-header-content] { align-items: center !important; line-height: 1 !important;} [data-diffs-header] [data-metadata] { align-items: center !important; line-height: 1 !important; font-variant-numeric: tabular-nums;} [data-diffs-header] [data-additions-count],[data-diffs-header] [data-deletions-count] { font-family: var(--font-mono) !important; font-size: 11px !important; font-variant-numeric: tabular-nums; line-height: 1 !important;} [data-diffs-header] [data-change-icon],[data-diffs-header] [data-rename-icon] { display: block; flex-shrink: 0;} [data-title] { cursor: pointer; transition: color 120ms ease, text-decoration-color 120ms ease; text-decoration: underline; text-decoration-color: transparent; text-underline-offset: 2px; font-family: var(--font-sans) !important;} [data-title]:hover { color: color-mix(in srgb, var(--code-foreground) 84%, var(--primary)) !important; text-decoration-color: currentColor;}`; interface DiffPanelProps { mode?: DiffPanelMode; composerDraftTarget: ScopedThreadRef | DraftId; initialGitScope: "branch" | "unstaged";} export { DiffWorkerPoolProvider } from "./DiffWorkerPoolProvider"; export default function DiffPanel({ mode = "inline", composerDraftTarget, initialGitScope: initialGitScopeProp,}: DiffPanelProps) { const { resolvedTheme } = useTheme(); const settings = useClientSettings(); const [initialGitScope] = useState(initialGitScopeProp); const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked"); const [wordWrap, setWordWrap] = useState(settings.wordWrap); const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace); const [baseRefQuery, setBaseRefQuery] = useState(""); const [collapsedDiffFiles, setCollapsedDiffFiles] = useState<CollapsedDiffFilesState>(() => ({ scopeKey: null, fileKeys: EMPTY_COLLAPSED_DIFF_FILE_KEYS, })); const [codeViewRevision, setCodeViewRevision] = useState(0); const codeViewRef = useRef<AnnotatableCodeViewHandle>(null); const lastCompletedTurnRefreshRef = useRef<{ readonly threadKey: string | null; readonly turnId: TurnId | null; } | 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 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]); useEffect(() => { const current = {Lines 1–500 of 1196. Load more
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, PilcrowIcon, RefreshCwIcon, Rows3Icon, SearchIcon, TextWrapIcon, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; 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 { useTheme } from "../hooks/useTheme"; import { buildFileDiffRenderKey, getDiffCollapseIconClassName, getDiffLineStat, getRenderablePatch, resolveDiffThemeName, resolveFileDiffPath, DIFF_SURFACE_THEME_UNSAFE_CSS, } from "../lib/diffRendering"; import { areAllDiffFilesCollapsed, toggleAllDiffFiles } from "../lib/diffCollapse"; import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; import { useProject, useThread } from "../state/entities"; import { resolveThreadRouteRef } from "../threadRoutes"; import { useClientSettings } from "../hooks/useSettings"; import { formatShortTimestamp } from "../timestampFormat"; import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell"; import { DiffStatLabel } from "./chat/DiffStatLabel"; import { AnnotatableCodeView, type AnnotatableCodeViewHandle } from "./diffs/AnnotatableCodeView"; 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"; type DiffRenderMode = "stacked" | "split"; type DiffThemeType = "light" | "dark"; const AUTOMATIC_BASE_REF = "__automatic_base_ref__"; interface CollapsedDiffFilesState { readonly scopeKey: string | null; readonly fileKeys: ReadonlySet<string>; } const EMPTY_COLLAPSED_DIFF_FILE_KEYS: ReadonlySet<string> = new Set(); const DIFF_PANEL_UNSAFE_CSS = `${DIFF_SURFACE_THEME_UNSAFE_CSS} :is( [data-line], [data-line-annotation], [data-merge-conflict], [data-merge-conflict-actions], [data-no-newline] )[data-selected-line] { --diffs-line-bg: light-dark( color-mix( in lab, var(--code-background) 88%, color-mix(in srgb, var(--code-background) 50%, var(--diffs-modified-base)) ), color-mix( in lab, var(--code-background) 80%, color-mix(in srgb, var(--code-background) 70%, var(--diffs-modified-base)) ) ) !important; } :is([data-gutter-buffer], [data-column-number])[data-selected-line] { --diffs-line-bg: light-dark( color-mix( in lab, var(--code-background) 91%, color-mix(in srgb, var(--code-background) 35%, var(--diffs-modified-base)) ), color-mix( in lab, var(--code-background) 85%, color-mix(in srgb, var(--code-background) 60%, var(--diffs-modified-base)) ) ) !important; } [data-indicators="bars"] :is([data-column-number], [data-gutter-buffer="annotation"])[data-selected-line] { position: relative; } [data-indicators="bars"] :is([data-column-number], [data-gutter-buffer="annotation"])[data-selected-line]::before { position: absolute !important; inset-block: 0 !important; inset-inline-start: 0 !important; display: block !important; width: 4px !important; min-width: 4px !important; max-width: 4px !important; height: auto !important; padding: 0 !important; content: "" !important; background-color: var(--diffs-modified-base) !important; background-image: none !important; } [data-file-info] { background-color: var(--code-background) !important; border-block-color: transparent !important; color: var(--code-foreground) !important; } [data-diffs-header] { position: sticky !important; top: 0; z-index: 4; background-color: var(--code-background) !important; border-bottom-color: transparent !important; align-items: center !important; font-family: var(--font-sans) !important; font-size: 12px !important; line-height: 1 !important; min-height: 32px !important; padding-block: 6px !important; padding-inline: 8px 12px !important; } [data-diffs-header]:hover { background-color: color-mix(in srgb, var(--code-background) 97%, var(--code-foreground)) !important; } :is([data-separator="line-info"], [data-separator="line-info-basic"]) { height: 24px !important; margin-block: 0 !important; background-color: var(--code-background) !important; } :is([data-separator="line-info"], [data-separator="line-info-basic"]) [data-separator-wrapper] { padding-inline: 8px 12px !important; background-color: transparent !important; } :is([data-separator="line-info"], [data-separator="line-info-basic"]) [data-separator-content] { gap: 8px; padding-inline: 0 !important; background-color: transparent !important; color: color-mix(in srgb, var(--code-foreground) 52%, var(--code-background)) !important; font-family: var(--font-sans) !important; font-size: 11px !important; text-decoration: none !important; } :is([data-separator="line-info"], [data-separator="line-info-basic"]) [data-unmodified-lines] { display: flex !important; min-width: 0; flex: 1 1 auto; align-items: center; gap: 8px; cursor: pointer; } :is([data-separator="line-info"], [data-separator="line-info-basic"]) [data-unmodified-lines]::before, :is([data-separator="line-info"], [data-separator="line-info-basic"]) [data-unmodified-lines]::after { width: auto; height: 1px; flex: 1 1 auto; content: ""; background-color: color-mix(in srgb, var(--code-background) 92%, var(--code-foreground)); } :is([data-separator="line-info"], [data-separator="line-info-basic"])[data-expand-index] [data-separator-wrapper] { grid-template-columns: 0 minmax(0, 1fr) !important; } :is([data-separator="line-info"], [data-separator="line-info-basic"])[data-expand-index] [data-separator-content] { grid-column: 2 !important; } :is([data-separator="line-info"], [data-separator="line-info-basic"]) [data-expand-button] { display: none !important; } :is([data-separator="line-info"], [data-separator="line-info-basic"]):has( [data-expand-button] ) [data-separator-content] { cursor: pointer; } :is([data-separator="line-info"], [data-separator="line-info-basic"]):has( [data-expand-button] ):hover [data-separator-content] { color: color-mix(in srgb, var(--code-foreground) 76%, var(--code-background)) !important; } :is([data-separator="line-info"], [data-separator="line-info-basic"]):has( [data-expand-button] ):hover [data-unmodified-lines]::before, :is([data-separator="line-info"], [data-separator="line-info-basic"]):has( [data-expand-button] ):hover [data-unmodified-lines]::after { background-color: color-mix(in srgb, var(--code-background) 84%, var(--code-foreground)); } [data-diffs-header] [data-header-content] { align-items: center !important; line-height: 1 !important; } [data-diffs-header] [data-metadata] { align-items: center !important; line-height: 1 !important; font-variant-numeric: tabular-nums; } [data-diffs-header] [data-additions-count], [data-diffs-header] [data-deletions-count] { font-family: var(--font-mono) !important; font-size: 11px !important; font-variant-numeric: tabular-nums; line-height: 1 !important; } [data-diffs-header] [data-change-icon], [data-diffs-header] [data-rename-icon] { display: block; flex-shrink: 0; } [data-title] { cursor: pointer; transition: color 120ms ease, text-decoration-color 120ms ease; text-decoration: underline; text-decoration-color: transparent; text-underline-offset: 2px; font-family: var(--font-sans) !important; } [data-title]:hover { color: color-mix(in srgb, var(--code-foreground) 84%, var(--primary)) !important; text-decoration-color: currentColor; } `; interface DiffPanelProps { mode?: DiffPanelMode; composerDraftTarget: ScopedThreadRef | DraftId; initialGitScope: "branch" | "unstaged"; } export { DiffWorkerPoolProvider } from "./DiffWorkerPoolProvider"; export default function DiffPanel({ mode = "inline", composerDraftTarget, initialGitScope: initialGitScopeProp, }: DiffPanelProps) { const { resolvedTheme } = useTheme(); const settings = useClientSettings(); const [initialGitScope] = useState(initialGitScopeProp); const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked"); const [wordWrap, setWordWrap] = useState(settings.wordWrap); const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace); const [baseRefQuery, setBaseRefQuery] = useState(""); const [collapsedDiffFiles, setCollapsedDiffFiles] = useState<CollapsedDiffFilesState>(() => ({ scopeKey: null, fileKeys: EMPTY_COLLAPSED_DIFF_FILE_KEYS, })); const [codeViewRevision, setCodeViewRevision] = useState(0); const codeViewRef = useRef<AnnotatableCodeViewHandle>(null); const lastCompletedTurnRefreshRef = useRef<{ readonly threadKey: string | null; readonly turnId: TurnId | null; } | 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 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]); useEffect(() => { const current = {