941 lines · 37.9 KB · TypeScript
RawDownload
import { useAtomValue } from "@effect/atom-react";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,  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,  getRenderablePatch,  resolveDiffThemeName,  resolveFileDiffPath,} 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 { 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 { 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 = `[data-diffs-header],[data-diff],[data-file],[data-error-wrapper],[data-virtualizer-buffer] {  --diffs-header-font-family: var(--font-sans) !important;  --diffs-font-family: var(--font-mono) !important;  --diffs-bg: color-mix(in srgb, var(--card) 90%, var(--background)) !important;  --diffs-light-bg: color-mix(in srgb, var(--card) 90%, var(--background)) !important;  --diffs-dark-bg: color-mix(in srgb, var(--card) 90%, var(--background)) !important;  --diffs-token-light-bg: transparent;  --diffs-token-dark-bg: transparent;   --diffs-bg-context-override: color-mix(in srgb, var(--background) 97%, var(--foreground));  --diffs-bg-hover-override: color-mix(in srgb, var(--background) 94%, var(--foreground));  --diffs-bg-separator-override: color-mix(in srgb, var(--background) 95%, var(--foreground));  --diffs-bg-buffer-override: color-mix(in srgb, var(--background) 90%, var(--foreground));   --diffs-bg-addition-override: color-mix(in srgb, var(--background) 92%, var(--success));  --diffs-bg-addition-number-override: color-mix(in srgb, var(--background) 88%, var(--success));  --diffs-bg-addition-hover-override: color-mix(in srgb, var(--background) 85%, var(--success));  --diffs-bg-addition-emphasis-override: color-mix(in srgb, var(--background) 80%, var(--success));   --diffs-bg-deletion-override: color-mix(in srgb, var(--background) 92%, var(--destructive));  --diffs-bg-deletion-number-override: color-mix(in srgb, var(--background) 88%, var(--destructive));  --diffs-bg-deletion-hover-override: color-mix(in srgb, var(--background) 85%, var(--destructive));  --diffs-bg-deletion-emphasis-override: color-mix(    in srgb,    var(--background) 80%,    var(--destructive)  );   background-color: var(--diffs-bg) !important;} [data-file-info] {  background-color: color-mix(in srgb, var(--card) 94%, var(--foreground)) !important;  border-block-color: var(--border) !important;  color: var(--foreground) !important;} [data-diffs-header] {  position: sticky !important;  top: 0;  z-index: 4;  background-color: color-mix(in srgb, var(--card) 94%, var(--foreground)) !important;  border-bottom: 1px solid var(--border) !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;} [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(--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 codeViewRef = useRef<AnnotatableCodeViewHandle>(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 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 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 selectedGitSource = branchDiffPreview.data?.sources.find(    (source) => source.kind === (selectedGitScope === "unstaged" ? "working-tree" : "branch-range"),  );  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 codeViewFiles = useMemo(    () =>      renderableFiles.map((fileDiff) => {        const fileKey = buildFileDiffRenderKey(fileDiff);        return {          fileDiff,          filePath: resolveFileDiffPath(fileDiff),          fileKey,          collapsed: collapsedDiffFileKeys.has(fileKey),        };      }),    [collapsedDiffFileKeys, renderableFiles],  );  const diffFileKeys = useMemo(() => codeViewFiles.map((file) => file.fileKey), [codeViewFiles]);  const allDiffFilesCollapsed = areAllDiffFilesCollapsed(diffFileKeys, collapsedDiffFileKeys);   useEffect(() => {    if (!selectedFilePath) return;    const file = codeViewFiles.find((candidate) => candidate.filePath === selectedFilePath);    if (!file) return;    codeViewRef.current?.scrollTo({ type: "item", id: file.fileKey, align: "start" });  }, [codeViewFiles, selectedFilePath, selectedFileRevealRequestId]);   const openDiffFile = useCallback(    (filePath: string) => {      openDiffFilePrimaryAction({        threadRef: routeThreadRef,        filePath,        activeCwd,        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)),              });            }          })();        },      });    },    [activeCwd, openInPreferredEditor, routeThreadRef],  );  const toggleDiffFileCollapsed = useCallback(    (fileKey: string) => {      setCollapsedDiffFiles((current) => {        const next = new Set(current.scopeKey === collapseScopeKey ? current.fileKeys : []);        if (next.has(fileKey)) {          next.delete(fileKey);        } else {          next.add(fileKey);        }        return { scopeKey: collapseScopeKey, fileKeys: next };      });

Lines 1–500 of 941.