Julius MarmingeT3 Code Mobile [WIP] (#2013)b3e8c035 months agoHistory
642 lines · 21.9 KB · TypeScript
RawDownload
import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime";import type { EnvironmentId, VcsRef, ThreadId } from "@t3tools/contracts";import { LegendList, type LegendListRef } from "@legendapp/list/react";import { ChevronDownIcon } from "lucide-react";import {  useCallback,  useDeferredValue,  useEffect,  useMemo,  useOptimistic,  useRef,  useState,  useTransition,} from "react"; import { useComposerDraftStore, type DraftId } from "../composerDraftStore";import { readEnvironmentApi } from "../environmentApi";import { useVcsStatus } from "../lib/vcsStatusState";import { useVcsRefs, vcsRefManager } from "../lib/vcsRefState";import { newCommandId } from "../lib/utils";import { cn } from "../lib/utils";import { parsePullRequestReference } from "../pullRequestReference";import { getSourceControlPresentation } from "../sourceControlPresentation";import { useStore } from "../store";import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors";import {  deriveLocalBranchNameFromRemoteRef,  resolveBranchSelectionTarget,  resolveBranchToolbarValue,  resolveDraftEnvModeAfterBranchChange,  resolveEffectiveEnvMode,  shouldIncludeBranchPickerItem,} from "./BranchToolbar.logic";import { Button } from "./ui/button";import {  Combobox,  ComboboxEmpty,  ComboboxInput,  ComboboxItem,  ComboboxList,  ComboboxListVirtualized,  ComboboxPopup,  ComboboxStatus,  ComboboxTrigger,} from "./ui/combobox";import { stackedThreadToast, toastManager } from "./ui/toast"; interface BranchToolbarBranchSelectorProps {  className?: string;  environmentId: EnvironmentId;  threadId: ThreadId;  draftId?: DraftId;  envLocked: boolean;  effectiveEnvModeOverride?: "local" | "worktree";  activeThreadBranchOverride?: string | null;  onActiveThreadBranchOverrideChange?: (refName: string | null) => void;  onCheckoutPullRequestRequest?: (reference: string) => void;  onComposerFocusRequest?: () => void;} const EMPTY_REFS: ReadonlyArray<VcsRef> = []; function toBranchActionErrorMessage(error: unknown): string {  return error instanceof Error ? error.message : "An error occurred.";} function getBranchTriggerLabel(input: {  activeWorktreePath: string | null;  effectiveEnvMode: "local" | "worktree";  resolvedActiveBranch: string | null;}): string {  const { activeWorktreePath, effectiveEnvMode, resolvedActiveBranch } = input;  if (!resolvedActiveBranch) {    return "Select ref";  }  if (effectiveEnvMode === "worktree" && !activeWorktreePath) {    return `From ${resolvedActiveBranch}`;  }  return resolvedActiveBranch;} export function BranchToolbarBranchSelector({  className,  environmentId,  threadId,  draftId,  envLocked,  effectiveEnvModeOverride,  activeThreadBranchOverride,  onActiveThreadBranchOverrideChange,  onCheckoutPullRequestRequest,  onComposerFocusRequest,}: BranchToolbarBranchSelectorProps) {  // ---------------------------------------------------------------------------  // Thread / project state (pushed down from parent to colocate with mutation)  // ---------------------------------------------------------------------------  const threadRef = useMemo(    () => scopeThreadRef(environmentId, threadId),    [environmentId, threadId],  );  const serverThreadSelector = useMemo(() => createThreadSelectorByRef(threadRef), [threadRef]);  const serverThread = useStore(serverThreadSelector);  const serverSession = serverThread?.session ?? null;  const setThreadBranchAction = useStore((store) => store.setThreadBranch);  const draftThread = useComposerDraftStore((store) =>    draftId ? store.getDraftSession(draftId) : store.getDraftThreadByRef(threadRef),  );  const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext);   const activeProjectRef = serverThread    ? scopeProjectRef(serverThread.environmentId, serverThread.projectId)    : draftThread      ? scopeProjectRef(draftThread.environmentId, draftThread.projectId)      : null;  const activeProjectSelector = useMemo(    () => createProjectSelectorByRef(activeProjectRef),    [activeProjectRef],  );  const activeProject = useStore(activeProjectSelector);   const activeThreadId = serverThread?.id ?? (draftThread ? threadId : undefined);  const activeThreadBranch =    activeThreadBranchOverride !== undefined      ? activeThreadBranchOverride      : (serverThread?.branch ?? draftThread?.branch ?? null);  const activeWorktreePath = serverThread?.worktreePath ?? draftThread?.worktreePath ?? null;  const activeProjectCwd = activeProject?.cwd ?? null;  const branchCwd = activeWorktreePath ?? activeProjectCwd;  const hasServerThread = serverThread !== undefined;  const effectiveEnvMode =    effectiveEnvModeOverride ??    resolveEffectiveEnvMode({      activeWorktreePath,      hasServerThread,      draftThreadEnvMode: draftThread?.envMode,    });   // ---------------------------------------------------------------------------  // Thread branch mutation (colocated — only this component calls it)  // ---------------------------------------------------------------------------  const setThreadBranch = useCallback(    (branch: string | null, worktreePath: string | null) => {      if (!activeThreadId || !activeProject) return;      const api = readEnvironmentApi(environmentId);      if (serverSession && worktreePath !== activeWorktreePath && api) {        void api.orchestration          .dispatchCommand({            type: "thread.session.stop",            commandId: newCommandId(),            threadId: activeThreadId,            createdAt: new Date().toISOString(),          })          .catch(() => undefined);      }      if (api && hasServerThread) {        void api.orchestration.dispatchCommand({          type: "thread.meta.update",          commandId: newCommandId(),          threadId: activeThreadId,          branch,          worktreePath,        });      }      if (hasServerThread) {        onActiveThreadBranchOverrideChange?.(branch);        setThreadBranchAction(threadRef, branch, worktreePath);        return;      }      const nextDraftEnvMode = resolveDraftEnvModeAfterBranchChange({        nextWorktreePath: worktreePath,        currentWorktreePath: activeWorktreePath,        effectiveEnvMode,      });      setDraftThreadContext(draftId ?? threadRef, {        branch,        worktreePath,        envMode: nextDraftEnvMode,        projectRef: scopeProjectRef(environmentId, activeProject.id),      });    },    [      activeThreadId,      activeProject,      serverSession,      activeWorktreePath,      hasServerThread,      onActiveThreadBranchOverrideChange,      setThreadBranchAction,      setDraftThreadContext,      draftId,      threadRef,      environmentId,      effectiveEnvMode,    ],  );   // ---------------------------------------------------------------------------  // Git ref queries  // ---------------------------------------------------------------------------  const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false);  const [branchQuery, setBranchQuery] = useState("");  const deferredBranchQuery = useDeferredValue(branchQuery);   const branchStatusQuery = useVcsStatus({ environmentId, cwd: branchCwd });  const trimmedBranchQuery = branchQuery.trim();  const deferredTrimmedBranchQuery = deferredBranchQuery.trim();  const branchRefTarget = useMemo(    () => ({      environmentId,      cwd: branchCwd,      query: deferredTrimmedBranchQuery,    }),    [branchCwd, deferredTrimmedBranchQuery, environmentId],  );  const branchRefState = useVcsRefs(branchRefTarget);  const refs = branchRefState.data?.refs ?? EMPTY_REFS;  const hasNextPage =    branchRefState.data?.nextCursor !== null && branchRefState.data?.nextCursor !== undefined;  const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);  const isInitialBranchesLoadPending = branchRefState.isPending && branchRefState.data === null;  const currentGitBranch =    branchStatusQuery.data?.refName ?? refs.find((refName) => refName.current)?.name ?? null;  const sourceControlPresentation = useMemo(    () => getSourceControlPresentation(branchStatusQuery.data?.sourceControlProvider),    [branchStatusQuery.data?.sourceControlProvider],  );  const SourceControlIcon = sourceControlPresentation.Icon;  const canonicalActiveBranch = resolveBranchToolbarValue({    envMode: effectiveEnvMode,    activeWorktreePath,    activeThreadBranch,    currentGitBranch,  });  const branchNames = useMemo(() => refs.map((refName) => refName.name), [refs]);  const branchByName = useMemo(    () => new Map(refs.map((refName) => [refName.name, refName] as const)),    [refs],  );  const normalizedDeferredBranchQuery = deferredTrimmedBranchQuery.toLowerCase();  const prReference = parsePullRequestReference(trimmedBranchQuery);  const isSelectingWorktreeBase =    effectiveEnvMode === "worktree" && !envLocked && !activeWorktreePath;  const checkoutPullRequestItemValue =    prReference && onCheckoutPullRequestRequest ? `__checkout_pull_request__:${prReference}` : null;  const canCreateBranch = !isSelectingWorktreeBase && trimmedBranchQuery.length > 0;  const hasExactBranchMatch = branchByName.has(trimmedBranchQuery);  const createBranchItemValue = canCreateBranch    ? `__create_new_branch__:${trimmedBranchQuery}`    : null;  const branchPickerItems = useMemo(() => {    const items = [...branchNames];    if (createBranchItemValue && !hasExactBranchMatch) {      items.push(createBranchItemValue);    }    if (checkoutPullRequestItemValue) {      items.unshift(checkoutPullRequestItemValue);    }    return items;  }, [branchNames, checkoutPullRequestItemValue, createBranchItemValue, hasExactBranchMatch]);  const filteredBranchPickerItems = useMemo(    () =>      normalizedDeferredBranchQuery.length === 0        ? branchPickerItems        : branchPickerItems.filter((itemValue) =>            shouldIncludeBranchPickerItem({              itemValue,              normalizedQuery: normalizedDeferredBranchQuery,              createBranchItemValue,              checkoutPullRequestItemValue,            }),          ),    [      branchPickerItems,      checkoutPullRequestItemValue,      createBranchItemValue,      normalizedDeferredBranchQuery,    ],  );  const [resolvedActiveBranch, setOptimisticBranch] = useOptimistic(    canonicalActiveBranch,    (_currentBranch: string | null, optimisticBranch: string | null) => optimisticBranch,  );  const [isBranchActionPending, startBranchActionTransition] = useTransition();  const shouldVirtualizeBranchList = filteredBranchPickerItems.length > 40;  const totalBranchCount = branchRefState.data?.totalCount ?? 0;  const branchStatusText = isInitialBranchesLoadPending    ? "Loading refs..."    : isFetchingNextPage      ? "Loading more refs..."      : hasNextPage        ? `Showing ${refs.length} of ${totalBranchCount} refs`        : null;   // ---------------------------------------------------------------------------  // Branch actions  // ---------------------------------------------------------------------------  const runBranchAction = (action: () => Promise<void>) => {    startBranchActionTransition(async () => {      await action().catch(() => undefined);      await vcsRefManager        .load(branchRefTarget, undefined, { limit: 100, preserveLoadedRefs: true })        .catch(() => undefined);    });  };   const selectBranch = (refName: VcsRef) => {    const api = readEnvironmentApi(environmentId);    if (!api || !branchCwd || !activeProjectCwd || isBranchActionPending) return;     if (isSelectingWorktreeBase) {      setThreadBranch(refName.name, null);      setIsBranchMenuOpen(false);      onComposerFocusRequest?.();      return;    }     const selectionTarget = resolveBranchSelectionTarget({      activeProjectCwd,      activeWorktreePath,      refName,    });     if (selectionTarget.reuseExistingWorktree) {      setThreadBranch(refName.name, selectionTarget.nextWorktreePath);      setIsBranchMenuOpen(false);      onComposerFocusRequest?.();      return;    }     const selectedBranchName = refName.isRemote      ? deriveLocalBranchNameFromRemoteRef(refName.name)      : refName.name;     setIsBranchMenuOpen(false);    onComposerFocusRequest?.();     runBranchAction(async () => {      const previousBranch = resolvedActiveBranch;      setOptimisticBranch(selectedBranchName);      try {        const checkoutResult = await api.vcs.switchRef({          cwd: selectionTarget.checkoutCwd,          refName: refName.name,        });        const nextBranchName = refName.isRemote          ? (checkoutResult.refName ?? selectedBranchName)          : selectedBranchName;        setOptimisticBranch(nextBranchName);        setThreadBranch(nextBranchName, selectionTarget.nextWorktreePath);      } catch (error) {        setOptimisticBranch(previousBranch);        toastManager.add(          stackedThreadToast({            type: "error",            title: "Failed to switch ref.",            description: toBranchActionErrorMessage(error),          }),        );      }    });  };   const createRef = (rawName: string) => {    const name = rawName.trim();    const api = readEnvironmentApi(environmentId);    if (!api || !branchCwd || !name || isBranchActionPending) return;     setIsBranchMenuOpen(false);    onComposerFocusRequest?.();     runBranchAction(async () => {      const previousBranch = resolvedActiveBranch;      setOptimisticBranch(name);      try {        const createBranchResult = await api.vcs.createRef({          cwd: branchCwd,          refName: name,          switchRef: true,        });        setOptimisticBranch(createBranchResult.refName);        setThreadBranch(createBranchResult.refName, activeWorktreePath);      } catch (error) {        setOptimisticBranch(previousBranch);        toastManager.add(          stackedThreadToast({            type: "error",            title: "Failed to create and switch ref.",            description: toBranchActionErrorMessage(error),          }),        );      }    });  };   useEffect(() => {    if (      effectiveEnvMode !== "worktree" ||      activeWorktreePath ||      activeThreadBranch ||      !currentGitBranch    ) {      return;    }    setThreadBranch(currentGitBranch, null);  }, [activeThreadBranch, activeWorktreePath, currentGitBranch, effectiveEnvMode, setThreadBranch]);   // ---------------------------------------------------------------------------  // Combobox / list plumbing  // ---------------------------------------------------------------------------  const handleOpenChange = useCallback(    (open: boolean) => {      setIsBranchMenuOpen(open);      if (!open) {        setBranchQuery("");        return;      }      void vcsRefManager        .load(branchRefTarget, undefined, { limit: 100, preserveLoadedRefs: true })        .catch(() => undefined);    },    [branchRefTarget],  );   const branchListScrollElementRef = useRef<HTMLDivElement | null>(null);  const fetchNextBranchPage = useCallback(() => {    if (!hasNextPage || isFetchingNextPage) {      return;    }     setIsFetchingNextPage(true);    void vcsRefManager      .loadNext(branchRefTarget, undefined, { limit: 100 })      .catch(() => undefined)      .finally(() => setIsFetchingNextPage(false));  }, [branchRefTarget, hasNextPage, isFetchingNextPage]);  const maybeFetchNextBranchPage = useCallback(() => {    if (!isBranchMenuOpen || !hasNextPage || isFetchingNextPage) {      return;    }     const scrollElement = branchListScrollElementRef.current;    if (!scrollElement) {      return;    }     const distanceFromBottom =      scrollElement.scrollHeight - scrollElement.scrollTop - scrollElement.clientHeight;    if (distanceFromBottom > 96) {      return;    }     fetchNextBranchPage();  }, [fetchNextBranchPage, hasNextPage, isBranchMenuOpen, isFetchingNextPage]);  const branchListRef = useRef<LegendListRef | null>(null);  const setBranchListRef = useCallback((element: HTMLDivElement | null) => {    branchListScrollElementRef.current = (element?.parentElement as HTMLDivElement | null) ?? null;  }, []);   useEffect(() => {    if (!isBranchMenuOpen) {      return;    }     if (shouldVirtualizeBranchList) {      branchListRef.current?.scrollToOffset?.({ offset: 0, animated: false });    } else {      branchListScrollElementRef.current?.scrollTo({ top: 0 });    }  }, [deferredTrimmedBranchQuery, isBranchMenuOpen, shouldVirtualizeBranchList]);   useEffect(() => {    const scrollElement = branchListScrollElementRef.current;    if (!scrollElement || !isBranchMenuOpen) {      return;    }     const handleScroll = () => {      maybeFetchNextBranchPage();    };     scrollElement.addEventListener("scroll", handleScroll, { passive: true });    handleScroll();    return () => {      scrollElement.removeEventListener("scroll", handleScroll);    };  }, [isBranchMenuOpen, maybeFetchNextBranchPage]);   useEffect(() => {    if (shouldVirtualizeBranchList) return;    maybeFetchNextBranchPage();  }, [refs.length, maybeFetchNextBranchPage, shouldVirtualizeBranchList]);   const triggerLabel = getBranchTriggerLabel({    activeWorktreePath,    effectiveEnvMode,    resolvedActiveBranch,  });   function renderPickerItem(itemValue: string, index: number) {    if (checkoutPullRequestItemValue && itemValue === checkoutPullRequestItemValue) {

Lines 1–500 of 642.