730 lines · 22.5 KB · TypeScript
RawDownload
import {  scopeProjectRef,  scopedThreadKey,  scopeThreadRef,} from "@t3tools/client-runtime/environment";import { pullRequestDetailToVcsStatus } from "@t3tools/client-runtime/state/pull-requests";import {  type EnvironmentId,  resolveEnvironmentMachineKind,  type ThreadLinkedPullRequest,  type VcsStatusResult,} from "@t3tools/contracts";import { Atom } from "effect/unstable/reactivity";import { FolderGit2Icon, TerminalIcon } from "lucide-react";import { useCallback, useMemo } from "react";import { appAtomRegistry } from "../rpc/atomRegistry";import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments";import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";import { useProject } from "../state/entities";import { useEnvironmentQuery } from "../state/query";import { linkedPullRequestDetailAtom, useSharedPullRequestSummary } from "../state/pullRequests";import { useThreadRunningTerminalIds } from "../state/terminalSessions";import { vcsEnvironment } from "../state/vcs";import { useUiStateStore } from "../uiStateStore";import { resolveChangeRequestPresentation } from "../sourceControlPresentation";import {  resolveThreadLastVisitedAt,  resolveThreadStatusPill,  type ThreadStatusPill,  useRetainedValue,  useSidebarRowSubscriptionLease,} from "./Sidebar.logic";import { resolvePullRequestState } from "./pullRequest/pullRequestPresentation";import type { SidebarThreadSummary } from "../types";import { formatWorktreePathForDisplay } from "../worktreeCleanup";import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; export interface PrStatusIndicator {  label: string;  colorClass: string;  tooltip: string;  tooltipLead: string;  tooltipTitle: string;  url: string;} export interface TerminalStatusIndicator {  label: "Terminal process running";  colorClass: string;  pulse: boolean;} export type ThreadPr = VcsStatusResult["pr"]; export interface LinkedThreadPullRequestStatus {  readonly pr: NonNullable<ThreadPr>;  readonly sourceControlProvider: NonNullable<VcsStatusResult["sourceControlProvider"]>;} export function useLinkedThreadPullRequest(  environmentId: EnvironmentId | null,  linkedPullRequest: ThreadLinkedPullRequest | null | undefined,  enabled = true,): LinkedThreadPullRequestStatus | null {  const queried = useEnvironmentQuery(    !enabled || environmentId === null || linkedPullRequest == null      ? null      : linkedPullRequestDetailAtom({          environmentId,          input: {            projectId: linkedPullRequest.projectId,            repository: linkedPullRequest.repository,            number: linkedPullRequest.number,          },        }),  ).data;  const detail = useSharedPullRequestSummary(environmentId, linkedPullRequest ?? null, queried);   return useMemo(    () =>      detail === null        ? null        : {            pr: pullRequestDetailToVcsStatus(detail),            sourceControlProvider: {              kind: detail.provider,              name: detail.provider,              baseUrl: "",            },          },    [detail],  );} export function settledPrHoverColorClass(  state: NonNullable<ThreadPr>["state"],  isDraft = false,): string {  switch (state) {    case "open":      if (isDraft) {        return "group-hover/v2-row:text-zinc-500 dark:group-hover/v2-row:text-zinc-400/80";      }      return "group-hover/v2-row:text-emerald-600 dark:group-hover/v2-row:text-emerald-300/90";    case "merged":      return "group-hover/v2-row:text-violet-600 dark:group-hover/v2-row:text-violet-300/90";    case "closed":      return "group-hover/v2-row:text-red-600 dark:group-hover/v2-row:text-red-300/90";  }} export function prStatusIndicator(  pr: ThreadPr,  provider: VcsStatusResult["sourceControlProvider"] | null | undefined,): PrStatusIndicator | null {  function formatPrState(pr: NonNullable<ThreadPr>): string {    if (pr.state === "open" && pr.isDraft === true) return "Draft";    return pr.state.charAt(0).toUpperCase() + pr.state.slice(1);  }   function formatPrStatusLead(pr: NonNullable<ThreadPr>, changeRequestShortName: string): string {    return `${changeRequestShortName} #${pr.number} - ${formatPrState(pr)}`;  }  if (!pr) return null;  const presentation = resolveChangeRequestPresentation(provider);   const tooltipLead = formatPrStatusLead(pr, presentation.shortName);  const tooltip = `${tooltipLead}: ${pr.title}`;   if (pr.state === "open") {    const isDraft = pr.isDraft === true;    return {      label: `${presentation.shortName} ${isDraft ? "draft" : "open"}`,      colorClass: isDraft        ? "text-zinc-500 dark:text-zinc-400/80"        : "text-emerald-600 dark:text-emerald-300/90",      tooltip,      tooltipLead,      tooltipTitle: pr.title,      url: pr.url,    };  }  if (pr.state === "closed") {    return {      label: `${presentation.shortName} closed`,      colorClass: "text-red-600 dark:text-red-300/90",      tooltip,      tooltipLead,      tooltipTitle: pr.title,      url: pr.url,    };  }  if (pr.state === "merged") {    return {      label: `${presentation.shortName} merged`,      colorClass: "text-violet-600 dark:text-violet-300/90",      tooltip,      tooltipLead,      tooltipTitle: pr.title,      url: pr.url,    };  }  return null;} export function ChangeRequestStatusIcon({  state,  isDraft = false,  className,}: Pick<NonNullable<ThreadPr>, "state"> & {  readonly isDraft?: boolean | undefined;  readonly className?: string | undefined;}) {  const presentation = resolvePullRequestState({ state, isDraft });  return <presentation.Icon className={className} />;} export function PrStatusTooltipContent({ status }: { status: PrStatusIndicator }) {  return (    <span className="flex max-w-[min(34rem,calc(100vw-2rem))] items-stretch overflow-hidden whitespace-nowrap">      <span className="shrink-0 pr-2 font-medium">{status.tooltipLead}</span>      <span className="min-h-4 shrink-0 border-border/70 border-l" aria-hidden="true" />      <span className="min-w-0 truncate pl-2">{status.tooltipTitle}</span>    </span>  );} export function resolveThreadPr(input: {  threadBranch: string | null;  gitStatus: VcsStatusResult | null;}): ThreadPr | null {  const { threadBranch, gitStatus } = input;  if (gitStatus === null) {    return null;  }   if (threadBranch === null || gitStatus.refName !== threadBranch) {    return null;  }   return gitStatus.pr ?? null;} /** * Parent-held PR snapshot for Sidebar V2. Rows remount when settlement * partitions move them, so terminal PR metadata must live above the row. */export interface ThreadChangeRequestSnapshot {  readonly branch: string;  readonly pr: NonNullable<ThreadPr>;  readonly sourceControlProvider: VcsStatusResult["sourceControlProvider"] | undefined;  readonly linkedPullRequest?: ThreadLinkedPullRequest;} export const threadChangeRequestSnapshotsAtom = Atom.make<  ReadonlyMap<string, ThreadChangeRequestSnapshot>>(new Map()).pipe(Atom.keepAlive, Atom.withLabel("sidebar:thread-change-request-snapshots")); function isTerminalChangeRequestState(  state: NonNullable<ThreadPr>["state"],): state is "merged" | "closed" {  return state === "merged" || state === "closed";} function sourceControlProvidersEqual(  left: VcsStatusResult["sourceControlProvider"] | undefined,  right: VcsStatusResult["sourceControlProvider"] | undefined,): boolean {  if (left === right) return true;  if (left == null || right == null) return left == null && right == null;  return left.kind === right.kind && left.name === right.name && left.baseUrl === right.baseUrl;} function linkedPullRequestsEqual(  left: ThreadLinkedPullRequest | null | undefined,  right: ThreadLinkedPullRequest | null | undefined,): boolean {  if (left == null || right == null) return left == null && right == null;  return (    left.projectId === right.projectId &&    left.repository === right.repository &&    left.number === right.number &&    left.url === right.url  );} export function threadChangeRequestSnapshotsEqual(  left: ThreadChangeRequestSnapshot,  right: ThreadChangeRequestSnapshot,): boolean {  return (    left.branch === right.branch &&    left.pr.number === right.pr.number &&    left.pr.title === right.pr.title &&    left.pr.url === right.pr.url &&    left.pr.baseRef === right.pr.baseRef &&    left.pr.headRef === right.pr.headRef &&    left.pr.state === right.pr.state &&    left.pr.isDraft === right.pr.isDraft &&    (left.pr.updatedAt ?? null) === (right.pr.updatedAt ?? null) &&    sourceControlProvidersEqual(left.sourceControlProvider, right.sourceControlProvider) &&    linkedPullRequestsEqual(left.linkedPullRequest, right.linkedPullRequest)  );} export function setThreadChangeRequestSnapshot(  threadKey: string,  snapshot: ThreadChangeRequestSnapshot | null,): void {  appAtomRegistry.modify(threadChangeRequestSnapshotsAtom, (current) => {    const existing = current.get(threadKey);    if (snapshot === null) {      if (existing === undefined) return [false, current];      const next = new Map(current);      next.delete(threadKey);      return [true, next];    }    if (existing !== undefined && threadChangeRequestSnapshotsEqual(existing, snapshot)) {      return [false, current];    }    const next = new Map(current);    next.set(threadKey, snapshot);    return [true, next];  });} /** * Authoritative snapshot update from live VCS status. * - `undefined`: missing status, or a local checkout retaining a terminal PR — leave the map alone * - `null`: no PR (without a retained terminal snapshot), a cleared branch, or a mismatch without a terminal PR — clear * - snapshot: matching branch reports a PR — store/replace */export function nextThreadChangeRequestSnapshot(input: {  threadBranch: string | null;  gitStatus: VcsStatusResult | null;  snapshot: ThreadChangeRequestSnapshot | null | undefined;  retainTerminalOnBranchMismatch: boolean;  linkedPullRequest?: ThreadLinkedPullRequest | null | undefined;  linkedPullRequestStatus?: LinkedThreadPullRequestStatus | null | undefined;}): ThreadChangeRequestSnapshot | null | undefined {  const {    threadBranch,    gitStatus,    snapshot,    retainTerminalOnBranchMismatch,    linkedPullRequest,    linkedPullRequestStatus,  } = input;  if (linkedPullRequest != null) {    if (linkedPullRequestStatus === null || linkedPullRequestStatus === undefined) {      return linkedPullRequestsEqual(snapshot?.linkedPullRequest, linkedPullRequest)        ? undefined        : null;    }    return {      branch: threadBranch ?? linkedPullRequestStatus.pr.headRef,      pr: linkedPullRequestStatus.pr,      sourceControlProvider: linkedPullRequestStatus.sourceControlProvider,      linkedPullRequest,    };  }  if (gitStatus === null) {    return snapshot?.linkedPullRequest === undefined ? undefined : null;  }  if (threadBranch === null) {    return null;  }  if (gitStatus.refName !== threadBranch) {    return retainTerminalOnBranchMismatch &&      snapshot != null &&      snapshot.linkedPullRequest === undefined &&      isTerminalChangeRequestState(snapshot.pr.state)      ? undefined      : null;  }  if (gitStatus.pr == null) {    if (      retainTerminalOnBranchMismatch &&      snapshot != null &&      snapshot.linkedPullRequest === undefined &&      isTerminalChangeRequestState(snapshot.pr.state)    ) {      return undefined;    }    return null;  }  return {    branch: threadBranch,    pr: gitStatus.pr,    sourceControlProvider: gitStatus.sourceControlProvider,  };} /** * Live PR when the checkout matches the thread branch; otherwise, for local * checkouts only, a cached merged/closed PR for the thread. Local thread * metadata follows the shared checkout, so the cached branch intentionally * survives that metadata changing to the newly checked-out branch. Open PRs * are retained only while live status is absent and the branch still matches. */export function resolveDisplayedThreadPr(input: {  threadBranch: string | null;  gitStatus: VcsStatusResult | null;  snapshot: ThreadChangeRequestSnapshot | null | undefined;  retainTerminalOnBranchMismatch: boolean;  linkedPullRequest?: ThreadLinkedPullRequest | null | undefined;  linkedPullRequestStatus?: LinkedThreadPullRequestStatus | null | undefined;}): ThreadPr | null {  const {    threadBranch,    gitStatus,    snapshot,    retainTerminalOnBranchMismatch,    linkedPullRequest,    linkedPullRequestStatus,  } = input;  if (linkedPullRequest != null) {    return (      linkedPullRequestStatus?.pr ??      (linkedPullRequestsEqual(snapshot?.linkedPullRequest, linkedPullRequest)        ? (snapshot?.pr ?? null)        : null)    );  }  if (    threadBranch !== null &&    gitStatus !== null &&    gitStatus.refName === threadBranch &&    gitStatus.pr != null  ) {    return gitStatus.pr;  }   if (    gitStatus === null &&    threadBranch !== null &&    snapshot?.branch === threadBranch &&    snapshot.linkedPullRequest === undefined  ) {    return snapshot.pr;  }   if (    threadBranch !== null &&    retainTerminalOnBranchMismatch &&    snapshot != null &&    snapshot.linkedPullRequest === undefined &&    isTerminalChangeRequestState(snapshot.pr.state)  ) {    return snapshot.pr;  }   return null;} export function resolveDisplayedThreadPrProvider(input: {  threadBranch: string | null;  gitStatus: VcsStatusResult | null;  snapshot: ThreadChangeRequestSnapshot | null | undefined;  retainTerminalOnBranchMismatch: boolean;  linkedPullRequest?: ThreadLinkedPullRequest | null | undefined;  linkedPullRequestStatus?: LinkedThreadPullRequestStatus | null | undefined;}): VcsStatusResult["sourceControlProvider"] | undefined {  const {    threadBranch,    gitStatus,    snapshot,    retainTerminalOnBranchMismatch,    linkedPullRequest,    linkedPullRequestStatus,  } = input;  if (linkedPullRequest != null) {    return (      linkedPullRequestStatus?.sourceControlProvider ??      (linkedPullRequestsEqual(snapshot?.linkedPullRequest, linkedPullRequest)        ? snapshot?.sourceControlProvider        : undefined)    );  }  if (    threadBranch !== null &&    gitStatus !== null &&    gitStatus.refName === threadBranch &&    gitStatus.pr != null  ) {    return gitStatus.sourceControlProvider;  }   if (    gitStatus === null &&    threadBranch !== null &&    snapshot?.branch === threadBranch &&    snapshot.linkedPullRequest === undefined  ) {    return snapshot.sourceControlProvider;  }   if (    threadBranch !== null &&    retainTerminalOnBranchMismatch &&    snapshot != null &&    snapshot.linkedPullRequest === undefined &&    isTerminalChangeRequestState(snapshot.pr.state)  ) {    return snapshot.sourceControlProvider;  }   return undefined;} export function terminalStatusFromRunningIds(  runningTerminalIds: ReadonlyArray<string>,): TerminalStatusIndicator | null {  if (runningTerminalIds.length === 0) {    return null;  }  return {    label: "Terminal process running",    colorClass: "text-teal-600 dark:text-teal-300/90",    pulse: true,  };} export function ThreadWorktreeIndicator({  thread,}: {  thread: Pick<SidebarThreadSummary, "id" | "branch" | "worktreePath">;}) {  const worktreePath = thread.worktreePath?.trim();  if (!worktreePath) {    return null;  }   const displayPath = formatWorktreePathForDisplay(worktreePath);  const tooltip = thread.branch    ? `Worktree: ${displayPath} (${thread.branch})`    : `Worktree: ${displayPath}`;   return (    <Tooltip>

Lines 1–500 of 730.