2408 lines · 102.6 KB · TypeScript
RawDownload
import { RefreshIcon } from "~/components/ui/refresh-icon";import { Spinner } from "~/components/ui/spinner";import { pullRequestHostOf, resolveEnvironmentMachineKind } from "@t3tools/contracts";import type {  EnvironmentId,  ProjectId,  PullRequestInvolvement,  PullRequestListCursors,  PullRequestListFilters,  PullRequestListInput,  PullRequestListResult,  PullRequestListState,  SourceControlProviderKind,} from "@t3tools/contracts";import { useAtomValue } from "@effect/atom-react";import { createFileRoute, useNavigate } from "@tanstack/react-router";import {  ArrowDownUpIcon,  CalendarArrowDownIcon,  CalendarArrowUpIcon,  ChevronDownIcon,  ClockIcon,  EyeIcon,  GitMergeIcon,  GitPullRequestClosedIcon,  GitPullRequestIcon,  LayersIcon,  ListChecksIcon,  PenLineIcon,  Maximize2Icon,  Minimize2Icon,  SearchIcon,} from "lucide-react";import {  useCallback,  useEffect,  useEffectEvent,  useMemo,  useRef,  useState,  type ReactNode,  type RefObject,} from "react"; import {  filterPullRequestsByInvolvement,  findScopedProject,  collectPullRequestListFacets,  groupPullRequestsByInvolvement,  matchesPullRequestFilters,  matchesPullRequestQuery,  parsePullRequestQuery,  narrowPullRequestsToFilters,  mergePullRequestDiffStats,  partitionPullRequestsWithPriority,  pullRequestDiffStatKey,  pullRequestEntryKey,  pullRequestEntryViewer,  rankPullRequestMatches,  sortPullRequestGroups,  pullRequestEnvironmentSetKey,  readPullRequestListSnapshot,  resolveProjectScope,  resolveQueryEnvironmentIds,  resolveSelectedEnvironmentId,  withDiffStat,  writePullRequestListSnapshot,  scorePullRequestMatch,  pullRequestStatsRefreshBatches,  pullRequestStatsRequestBatches,  retainVisiblePullRequestStatsBatches,  type EnvironmentPullRequestEntry,  type MergedPullRequestList,  type PullRequestDiffStats,  type PullRequestStatsBatch,  type PullRequestStatsPolicy,  type PullRequestStatsScope,  type PullRequestPartitionsSnapshot,} from "../components/pullRequest/pullRequestList.logic";import {  pullRequestListPreferences,  type PullRequestListPreferencePatch,  type PullRequestListPreferences,  type PullRequestListSort,  writePullRequestListPreferences,} from "../components/pullRequest/pullRequestListPreferences";import { assignProjectsToEnvironments } from "../components/pullRequest/pullRequestProjectAssignment.logic";import { pullRequestFilterProjects } from "../components/pullRequest/pullRequestProjectFilter.logic";import { environmentMachineIcon } from "../components/EnvironmentMachineIcon";import { PullRequestDetailPanel } from "../components/pullRequest/PullRequestDetailPanel";import {  PullRequestFiltersMenu,  PullRequestFilterOptionIcon,  PullRequestSearchInput,  pullRequestHostLabel,  pullRequestProjectKey,  type PullRequestExpectedHost,  type PullRequestFilterOption,} from "../components/pullRequest/PullRequestListFilters";import { PullRequestListEmptyState } from "../components/pullRequest/PullRequestListEmptyState";import { PullRequestListGhost } from "../components/pullRequest/PullRequestGhosts";import {  PullRequestRow,  type PullRequestRowTarget,} from "../components/pullRequest/PullRequestRow";import { PullRequestsUnavailableState } from "../components/pullRequest/PullRequestsUnavailableState";import { RightPanelTabs, type PullRequestTabStatusSeed } from "../components/RightPanelTabs";import {  WorkspaceBreadcrumb,  WorkspaceBreadcrumbItem,  WorkspaceBreadcrumbSeparator,} from "../components/WorkspaceBreadcrumb";import { WorkspacePageContainer } from "../components/WorkspacePageContainer";import { WorkspacePageHeader } from "../components/WorkspacePageHeader";import { isCommandPaletteOpen } from "../commandPaletteBus";import { isElectron } from "../env";import { resolveShortcutCommand } from "../keybindings";import { isTerminalFocused } from "../lib/terminalFocus";import { PanelLayoutControls } from "../components/chat/PanelLayoutControls";import { Button } from "../components/ui/button";import { Menu, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "../components/ui/menu";import { SidebarInset } from "../components/ui/sidebar";import { Tooltip, TooltipPopup, TooltipTrigger } from "../components/ui/tooltip";import { useLiveRefresh } from "../hooks/useLiveRefresh";import { useOpenPanelPullRequestUrl } from "../hooks/useOpenPanelPullRequestUrl";import { writeTextToClipboard } from "../hooks/useCopyToClipboard";import { toastManager } from "../components/ui/toast";import { usePanelAnimationSettings, usePanelPresence } from "../panelAnimations";import {  PULL_REQUESTS_PANEL_REF,  pullRequestSurfaceId,  selectActiveRightPanelSurface,  selectSelectedRightPanelSurface,  selectThreadRightPanelState,  useRightPanelStore,  type PullRequestSurface,} from "../rightPanelStore";import { useDebouncedValue } from "../state/queries";import { useAllEnvironmentShellsBootstrapped, useProjects } from "../state/entities";import { useEnvironments } from "../state/environments";import {  pullRequestEnvironment,  usePullRequestList,  usePullRequestListStats,  usePullRequestTurnRefreshes,  type EnvironmentQueryTarget,} from "../state/pullRequests";import { useAtomCommand } from "../state/use-atom-command";import { cn } from "~/lib/utils";import { primaryServerKeybindingsAtom } from "~/state/server";import { getSourceControlPresentationForKind } from "~/sourceControlPresentation"; export interface PullRequestsSearch extends PullRequestListPreferences {  /**   * Narrows the list to one server. Absent means every connected one, which is the default the   * page has now — so a link written before servers could be chosen still opens the whole list.   */  readonly environmentId?: EnvironmentId;  /** Scopes the list. Separate from the selection so one cannot silently change the other. */  readonly projectId?: ProjectId;  /**   * Narrows the list to one host, named as the host itself: two GitHub installs are two   * accounts, and their shared provider kind cannot tell them apart. Absent means every host.   */  readonly host?: string;  readonly repository?: string;  readonly number?: number;  readonly selectedProjectId?: ProjectId;  /** Host of the open review; changing tabs must not narrow the list host filter. */  readonly selectedHost?: string;  /**   * Which server the selected pull request was read from. A project id only names a project on   * its own server, so this is what tells two servers holding one project apart. Optional: a   * link without it still opens, resolved by project id alone where that is unambiguous.   */  readonly selectedEnvironmentId?: EnvironmentId;} // The state filters wear the same glyphs the rows do, so the two read as one vocabulary.const INVOLVEMENT_TABS = [  { value: "all", label: "All", Icon: LayersIcon },  { value: "reviewing", label: "Reviewing", Icon: EyeIcon },  { value: "authored", label: "Authored", Icon: PenLineIcon },] as const satisfies ReadonlyArray<PullRequestFilterOption<PullRequestInvolvement>>; const STATE_TABS = [  { value: "all", label: "All", Icon: LayersIcon },  { value: "open", label: "Open", Icon: GitPullRequestIcon },  { value: "closed", label: "Closed", Icon: GitPullRequestClosedIcon },  { value: "merged", label: "Merged", Icon: GitMergeIcon },] as const satisfies ReadonlyArray<PullRequestFilterOption<PullRequestListState>>; const SORT_OPTIONS = [  { value: "ready", label: "Merge readiness", Icon: ListChecksIcon },  { value: "updated", label: "Recently updated", Icon: ClockIcon },  { value: "newest", label: "Newest shown", Icon: CalendarArrowDownIcon },  { value: "oldest", label: "Oldest shown", Icon: CalendarArrowUpIcon },  { value: "largest", label: "Largest shown", Icon: Maximize2Icon },  { value: "smallest", label: "Smallest shown", Icon: Minimize2Icon },] as const satisfies ReadonlyArray<PullRequestFilterOption<PullRequestListSort>>; /** Long enough that a keystroke does not become a request, short enough to feel answered. */const SEARCH_DEBOUNCE_MS = 250;/** What `scorePullRequestMatch` gives a row none of whose own fields carry the search text. */const MATCHED_ELSEWHERE_SCORE = 10;/** * One whole page from the host and no more: every provider asks for one row beyond the page as * its "is there more" probe, and GitHub serves a hundred per request — so asking for ninety-nine * costs one round trip where a hundred costs two. */const PAGE_SIZE = 99;/** The largest page the listing accepts; past it the request is refused outright. */const MAX_PAGE_SIZE = 500;/** Stable empty map so the memos below do not see a new object on every render. */const EMPTY_VIEWERS: PullRequestListResult["viewers"] = {};/** Stable so a read that is not wanted right now does not re-key on every render. */const NO_LIST_TARGETS: ReadonlyArray<EnvironmentQueryTarget<PullRequestListInput>> = [];const EMPTY_PREVIEW_SESSIONS = {};const EMPTY_PREVIEW_DESKTOP_STATE = {};const EMPTY_TERMINAL_LABELS = new Map<string, string>();const EMPTY_PENDING_SURFACES = new Set<string>();const MAX_SEARCH_LABEL_CANDIDATES = 100; const pullRequestListEntryId = (target: Parameters<typeof pullRequestSurfaceId>[0]) =>  pullRequestSurfaceId({ ...target, repository: target.repository.toLowerCase() }); function pullRequestSearchLabels(raw: unknown): Partial<Pick<PullRequestsSearch, "labels">> {  const values = (Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : []).slice(    0,    MAX_SEARCH_LABEL_CANDIDATES,  );  const labels: Array<string> = [];  const seen = new Set<string>();  for (const rawValue of values) {    if (typeof rawValue !== "string") continue;    const value = rawValue.trim().slice(0, 200);    const key = value.toLowerCase();    if (value.length === 0 || seen.has(key)) continue;    labels.push(value);    seen.add(key);    if (labels.length === 10) break;  }  return labels.length === 0 ? {} : { labels };} export const Route = createFileRoute("/_chat/pull-requests")({  validateSearch: (raw: Record<string, unknown>): PullRequestsSearch => ({    involvement:      raw.involvement === "reviewing" || raw.involvement === "authored" ? raw.involvement : "all",    state:      raw.state === "closed" || raw.state === "merged" || raw.state === "all" ? raw.state : "open",    ...(SORT_OPTIONS.some((option) => option.value === raw.sort)      ? { sort: raw.sort as PullRequestListSort }      : {}),    ...(typeof raw.repository === "string" && raw.repository      ? { repository: raw.repository.slice(0, 200) }      : {}),    ...(typeof raw.number === "number" && Number.isInteger(raw.number) && raw.number > 0      ? { number: raw.number }      : {}),    ...(typeof raw.projectId === "string" && raw.projectId      ? { projectId: raw.projectId as ProjectId }      : {}),    ...(typeof raw.environmentId === "string" && raw.environmentId      ? { environmentId: raw.environmentId as EnvironmentId }      : {}),    ...(typeof raw.host === "string" && raw.host ? { host: raw.host.slice(0, 200) } : {}),    ...(typeof raw.selectedProjectId === "string" && raw.selectedProjectId      ? { selectedProjectId: raw.selectedProjectId as ProjectId }      : {}),    ...(typeof raw.selectedHost === "string" && raw.selectedHost      ? { selectedHost: raw.selectedHost.slice(0, 200) }      : {}),    ...(typeof raw.selectedEnvironmentId === "string" && raw.selectedEnvironmentId      ? { selectedEnvironmentId: raw.selectedEnvironmentId as EnvironmentId }      : {}),    ...(typeof raw.q === "string" && raw.q ? { q: raw.q.slice(0, 200) } : {}),    ...(raw.draft === "only" || raw.draft === "hide" ? { draft: raw.draft } : {}),    ...(raw.review === "approved" ||    raw.review === "changes-requested" ||    raw.review === "review-required" ||    raw.review === "none"      ? { review: raw.review }      : {}),    ...(raw.checks === "passing" || raw.checks === "failing" ? { checks: raw.checks } : {}),    ...(typeof raw.author === "string" && raw.author.trim()      ? { author: raw.author.trim().slice(0, 200) }      : {}),    ...pullRequestSearchLabels(raw.labels),  }),  component: PullRequestsRouteView,}); function PullRequestsRouteView() {  const search = Route.useSearch();  const sort = search.sort ?? "ready";  const statsPolicy: PullRequestStatsPolicy =    sort === "ready" || sort === "largest" || sort === "smallest" ? "eager" : "visible";  const navigate = useNavigate({ from: Route.fullPath });  const keybindings = useAtomValue(primaryServerKeybindingsAtom);  const { environments } = useEnvironments();  // Every connected environment that has said it can list pull requests. Sorted, so the query  // keys, the scope key and the stored snapshot all read the same whichever order the  // connections happened to come up in.  const capableEnvironments = useMemo(    () =>      environments        .filter(          (environment) => environment.serverConfig?.environment.capabilities.pullRequests === true,        )        .toSorted((left, right) => left.environmentId.localeCompare(right.environmentId)),    [environments],  );  // The server the URL asks for, kept only while it is one the page could read: a link naming a  // server this workspace no longer has falls back to all of them rather than to nothing.  const scopedEnvironmentId =    capableEnvironments.find((environment) => environment.environmentId === search.environmentId)      ?.environmentId ?? null;  // Every server this workspace has ever heard of, connecting or not — wider than  // `capableEnvironments`, which only holds the ones ready to answer.  const knownEnvironmentIds = useMemo(    () => new Set(environments.map((environment) => environment.environmentId)),    [environments],  );  const environmentIds = useMemo(    () =>      capableEnvironments        .filter(          (environment) =>            scopedEnvironmentId === null || environment.environmentId === scopedEnvironmentId,        )        .map((environment) => environment.environmentId),    [capableEnvironments, scopedEnvironmentId],  );  const environmentKey = useMemo(    () => pullRequestEnvironmentSetKey(environmentIds),    [environmentIds],  );  // An environment may still be connecting, or may predate this feature. Until at least one has  // reported, an empty set means "not known yet" rather than "no environment can", and the page  // waits rather than telling a reader to upgrade a server that has not spoken.  const capabilityKnown = environments.some((environment) => environment.serverConfig !== null);  const pullRequestsSupported = environmentIds.length > 0;  const allProjects = useProjects();  // Whether the workspace has said what it holds yet. Until it has, an empty project list is  // "not loaded" rather than "none", and telling a reader to add a project they already have is  // the one wrong answer the empty state can give.  const projectsKnown = useAllEnvironmentShellsBootstrapped();  // Only the projects the page can actually read: one on an environment that cannot list pull  // requests could neither be listed nor acted on.  const projects = useMemo(    () => allProjects.filter((project) => environmentIds.includes(project.environmentId)),    [allProjects, environmentIds],  );  const environmentLabels = useMemo(    () =>      new Map(        environments.map((environment) => [environment.environmentId, environment.label] as const),      ),    [environments],  );  // The scope the URL asks for, once the environments have had their say about whether it exists.  const scopedProjectId = useMemo(    () => resolveProjectScope(search.projectId, projects, projectsKnown),    [projects, projectsKnown, search.projectId],  );  const scopedProject = useMemo(    () => findScopedProject(projects, scopedEnvironmentId, scopedProjectId),    [projects, scopedEnvironmentId, scopedProjectId],  );  const scopedProjects = useMemo(    () => pullRequestFilterProjects(projects, environmentLabels, scopedProject),    [environmentLabels, projects, scopedProject],  );   // A link from a thread or the sidebar only knows the repository, so the owning project is  // resolved here; an explicit `projectId` in the URL still wins.  const selectedHost = search.selectedHost ?? search.host;  const projectIdForRepository = useMemo(() => {    const repository = search.repository?.toLowerCase();    if (repository === undefined) return undefined;    const identity = projects.find(      (project) =>        project.repositoryIdentity?.owner &&        project.repositoryIdentity.name &&        `${project.repositoryIdentity.owner}/${project.repositoryIdentity.name}`.toLowerCase() ===          repository &&        // The same `owner/name` can exist on two hosts. Without this the first match wins, and        // a link that named its host opens the pull request from the other one.        (selectedHost === undefined ||          pullRequestHostOf(            project.repositoryIdentity,            project.repositoryIdentity.provider as SourceControlProviderKind,          ) === selectedHost.toLowerCase()),    );    return identity?.id;  }, [projects, selectedHost, search.repository]);   // The selection is resolved the same way the scope is: an id no connected environment has can  // never be read here, and one that arrived before the projects did is not yet wrong.  const linkedProjectId = useMemo(    () => resolveProjectScope(search.selectedProjectId, projects, projectsKnown),    [projects, projectsKnown, search.selectedProjectId],  );  // The scope filter stands in as a last resort: a link can carry `projectId` with a repository  // whose identity the inference above cannot match, and refusing to open it because of the  // weaker signal would ignore the stronger one the URL spelled out.  const selectedProjectId = linkedProjectId ?? projectIdForRepository ?? scopedProjectId;  // Which server the selection belongs to. Named by the URL where a link had one to give;  // otherwise the project id decides, and only where exactly one server has that project.  // A named server still connecting is kept named rather than falling back: falling back while  // it is merely not ready yet would resolve the project against whatever other server has  // answered, which can be the wrong one where two servers share a project id. Only a name this  // workspace has never heard of falls back to the scope above, the same way that scope treats a  // stale name — a saved link naming a server that has since gone would otherwise open nothing,  // even where the project id it also carries names exactly one remaining server.  const selectedEnvironmentId = resolveSelectedEnvironmentId(    search.selectedEnvironmentId,    knownEnvironmentIds,    scopedEnvironmentId,  );  const selectedProject = useMemo(    () => findScopedProject(projects, selectedEnvironmentId, selectedProjectId),    [projects, selectedEnvironmentId, selectedProjectId],  );  // One panel for the page rather than one per server: the surfaces carry the server they were  // read from, so tabs from two of them sit side by side instead of replacing each other. Its ref  // uses a fixed sentinel environment, not whichever server happens to sort first, so the tab  // strip survives a capable server disconnecting or losing the pull-requests capability.  const rightPanelRef = capableEnvironments.length === 0 ? null : PULL_REQUESTS_PANEL_REF;  const openPanelPullRequestUrl = useOpenPanelPullRequestUrl(rightPanelRef);  const rightPanelState = useRightPanelStore((state) =>    selectThreadRightPanelState(state.byThreadKey, rightPanelRef),  );  const selectedRightPanelSurface = useRightPanelStore((state) =>    selectSelectedRightPanelSurface(state.byThreadKey, rightPanelRef),  );  const selectedPullRequestSurface =    selectedRightPanelSurface?.kind === "pull-request" ? selectedRightPanelSurface : null;  const activePullRequestSurface = rightPanelState.isOpen ? selectedPullRequestSurface : null;  const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } =    usePanelAnimationSettings();  const rightPanelPresenceValue = useMemo(    () => ({      activeSurface: selectedPullRequestSurface,      surfaces: rightPanelState.surfaces,    }),    [rightPanelState.surfaces, selectedPullRequestSurface],  );  const rightPanelPresence = usePanelPresence(    rightPanelState.isOpen && selectedPullRequestSurface !== null,    rightPanelPresenceValue,    panelAnimationsActive,    rightPanelRef?.threadId ?? null,    panelAnimationDurationMs,  );  const rightPanelPresent = rightPanelPresence.present;  const renderedPullRequestSurface = rightPanelPresence.value?.activeSurface ?? null;  const renderedRightPanelSurfaces = rightPanelPresence.value?.surfaces ?? [];  // The open tab names its own server; a link that arrived before any tab was opened names it  // through the project it selected.  const panelEnvironmentId =    (renderedPullRequestSurface?.environmentId as EnvironmentId | undefined) ??    selectedProject?.environmentId ??    null;  const updateSearch = useCallback(    (patch: {      [Key in keyof PullRequestsSearch]?: PullRequestsSearch[Key] | undefined;    }) =>      void navigate({        // Rebuilt rather than spread so a cleared field leaves the URL instead of        // lingering as an explicit `undefined`.        search: (previous: PullRequestsSearch): PullRequestsSearch => {          const next = { ...previous, ...patch };          return {            involvement: next.involvement ?? previous.involvement,            state: next.state ?? previous.state,            ...(next.sort && next.sort !== "ready" ? { sort: next.sort } : {}),            ...(next.repository ? { repository: next.repository } : {}),            ...(next.number ? { number: next.number } : {}),            ...(next.projectId ? { projectId: next.projectId } : {}),            ...(next.environmentId ? { environmentId: next.environmentId } : {}),            ...(next.host ? { host: next.host } : {}),            ...(next.selectedHost ? { selectedHost: next.selectedHost } : {}),            ...(next.selectedProjectId ? { selectedProjectId: next.selectedProjectId } : {}),            ...(next.selectedEnvironmentId              ? { selectedEnvironmentId: next.selectedEnvironmentId }              : {}),            ...(next.q ? { q: next.q } : {}),            ...(next.draft ? { draft: next.draft } : {}),            ...(next.review ? { review: next.review } : {}),            ...(next.checks ? { checks: next.checks } : {}),            ...(next.author ? { author: next.author } : {}),            ...(next.labels && next.labels.length > 0 ? { labels: next.labels } : {}),          };        },        replace: true,      }),    [navigate],  );

Lines 1–500 of 2408.