1242 lines · 47.0 KB · TypeScript
RawDownload
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,} 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 = `[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: var(--background) !important;  --diffs-light-bg: var(--background) !important;  --diffs-dark-bg: 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: light-dark(    color-mix(in srgb, var(--background) 50%, var(--success)),    color-mix(in srgb, var(--background) 70%, var(--success))  );  --diffs-bg-addition-number-override: light-dark(    color-mix(in srgb, var(--background) 35%, var(--success)),    color-mix(in srgb, var(--background) 60%, 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: light-dark(    color-mix(in srgb, var(--background) 50%, var(--destructive)),    color-mix(in srgb, var(--background) 70%, var(--destructive))  );  --diffs-bg-deletion-number-override: light-dark(    color-mix(in srgb, var(--background) 35%, var(--destructive)),    color-mix(in srgb, var(--background) 60%, 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;} :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(--background) 88%,      color-mix(in srgb, var(--background) 50%, var(--diffs-modified-base))    ),    color-mix(      in lab,      var(--background) 80%,      color-mix(in srgb, var(--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(--background) 91%,      color-mix(in srgb, var(--background) 35%, var(--diffs-modified-base))    ),    color-mix(      in lab,      var(--background) 85%,      color-mix(in srgb, var(--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(--background) !important;  border-block-color: transparent !important;  color: var(--foreground) !important;} [data-diffs-header] {  position: sticky !important;  top: 0;  z-index: 4;  background-color: var(--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(--background) 97%, var(--foreground)) !important;} :is([data-separator="line-info"], [data-separator="line-info-basic"]) {  height: 24px !important;  margin-block: 0 !important;  background-color: var(--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(--foreground) 52%, var(--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(--background) 92%, var(--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(--foreground) 76%, var(--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(--background) 84%, var(--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(--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

Lines 1–500 of 1242.