import { RefreshIcon } from "~/components/ui/refresh-icon"; import { ChevronDownIcon, GitPullRequestIcon } from "lucide-react"; import * as Duration from "effect/Duration"; import * as Option from "effect/Option"; import { useEffect, useState, type ReactNode } from "react"; import type { BackgroundActivitySettings, SourceControlProviderKind, SourceControlDiscoveryResult, SourceControlProviderAuth, SourceControlProviderDiscoveryItem, VcsDriverKind, VcsDiscoveryItem, } from "@t3tools/contracts"; import { getBackgroundActivityBaseProfile, getBackgroundActivityPresetSettings, resolveServerBackgroundActivitySettings, } from "@t3tools/shared/backgroundActivitySettings"; import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings"; import { SharedSettingsMismatchAlert } from "./SharedSettingsMismatchAlert"; import { cn } from "../../lib/utils"; import { useEnvironments, usePrimaryEnvironment } from "../../state/environments"; import { useEnvironmentQuery } from "../../state/query"; import { sourceControlEnvironment } from "../../state/sourceControl"; import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; import { Collapsible, CollapsibleContent } from "../ui/collapsible"; import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from "../ui/empty"; import { Skeleton } from "../ui/skeleton"; import { NumberField, NumberFieldDecrement, NumberFieldGroup, NumberFieldIncrement, NumberFieldInput, } from "../ui/number-field"; import { Switch } from "../ui/switch"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitIcon, GitLabIcon, JujutsuIcon, type Icon, } from "../Icons"; import { RedactedSensitiveText } from "./RedactedSensitiveText"; import { SourceControlWritingSettingsSection } from "./SourceControlWritingSettings"; import { PolicyTooltip, SettingResetButton, SettingsPageContainer, SettingsSearchTarget, SettingsSection, useSettingsSearchTargetId, } from "./settingsLayout"; import { searchableSetting } from "./settingsSearch"; const EMPTY_DISCOVERY_RESULT: SourceControlDiscoveryResult = { versionControlSystems: [], sourceControlProviders: [], }; const SOURCE_CONTROL_PROVIDER_ICONS: Partial> = { github: GitHubIcon, gitlab: GitLabIcon, "azure-devops": AzureDevOpsIcon, bitbucket: BitbucketIcon, }; const VCS_ICONS: Partial> = { git: GitIcon, jj: JujutsuIcon, }; const SOURCE_CONTROL_SKELETON_ROWS = ["primary", "secondary"] as const; const GIT_FETCH_INTERVAL_STEP_SECONDS = 5; type BackgroundActivityOverridePatch = Partial<{ [K in keyof BackgroundActivitySettings["overrides"]]: | BackgroundActivitySettings["overrides"][K] | undefined; }>; function durationToSeconds(duration: Duration.Duration): number { return Math.round(Duration.toMillis(duration) / 1_000); } function normalizeFetchIntervalSeconds(value: number | null): number { if (value === null || !Number.isFinite(value)) { return 0; } return Math.max(0, Math.round(value)); } function backgroundActivityOverrideSettings( current: BackgroundActivitySettings, overrides: BackgroundActivityOverridePatch, ) { const nextOverrides: BackgroundActivityOverridePatch = { ...current.overrides, ...overrides, }; for (const [key, value] of Object.entries(nextOverrides)) { if (value === undefined) { delete nextOverrides[key as keyof typeof nextOverrides]; } } return { backgroundActivity: { schemaVersion: 1 as const, profile: "custom" as const, baseProfile: getBackgroundActivityBaseProfile(current), overrides: nextOverrides as BackgroundActivitySettings["overrides"], }, }; } function optionLabel(value: Option.Option): string | null { return Option.getOrNull(value); } function isProviderDiscoveryItem( item: VcsDiscoveryItem | SourceControlProviderDiscoveryItem, ): item is SourceControlProviderDiscoveryItem { return "auth" in item; } function isVcsNotReady(item: VcsDiscoveryItem | SourceControlProviderDiscoveryItem): boolean { return !isProviderDiscoveryItem(item) && !item.implemented; } function authPresentation(auth: SourceControlProviderAuth): { readonly label: string; readonly badge: "warning" | null; } { if (auth.status === "authenticated") { return { label: "Authenticated", badge: null }; } if (auth.status === "unauthenticated") { return { label: "Not authenticated", badge: "warning" }; } return { label: "Status unknown", badge: null }; } function RedactedAccount(props: { readonly account: string | null }) { return ( ); } function itemStatusDot(item: VcsDiscoveryItem | SourceControlProviderDiscoveryItem): string { if (isVcsNotReady(item)) return "bg-muted-foreground/35"; if (item.status !== "available") return "bg-warning"; if (isProviderDiscoveryItem(item) && item.auth.status !== "authenticated") return "bg-warning"; return "bg-success"; } function SourceControlItemMark({ item, }: { readonly item: VcsDiscoveryItem | SourceControlProviderDiscoveryItem; }) { const dotClassName = itemStatusDot(item); const Icon = isProviderDiscoveryItem(item) ? SOURCE_CONTROL_PROVIDER_ICONS[item.kind] : VCS_ICONS[item.kind]; if (!Icon) { return ; } return ( ); } function itemSummary({ item, auth, authAccount, }: { readonly item: VcsDiscoveryItem | SourceControlProviderDiscoveryItem; readonly auth: SourceControlProviderAuth | null; readonly authAccount: string | null; }) { if (isVcsNotReady(item)) { return Support for {item.label} is coming soon.; } if (item.status !== "available") { return Not available on this server: {item.installHint}; } if (auth) { if (auth.status === "authenticated") { return ( <> Authenticated {authAccount ? ( <> as ) : null} ); } if (!item.executable) { return Available. {item.installHint}; } if (auth.status === "unauthenticated") { return ( {item.label} is not authenticated on this server. Sign in or configure credentials using the {item.executable}{" "} tool on the server host to enable change request features. ); } const authDetail = optionLabel(auth.detail); return ( Could not verify {item.label}. {authDetail ?? item.installHint} ); } return Available; } function DiscoveryItemRow({ item, children, }: { readonly item: VcsDiscoveryItem | SourceControlProviderDiscoveryItem; readonly children?: ReactNode; }) { const version = optionLabel(item.version); const enabled = isProviderDiscoveryItem(item) ? item.status === "available" && item.auth.status === "authenticated" : item.status === "available" && item.implemented; const auth = isProviderDiscoveryItem(item) ? item.auth : null; const authStatus = auth ? authPresentation(auth) : null; const authAccount = auth ? optionLabel(auth.account) : null; const [isExpanded, setIsExpanded] = useState(false); const hasDetails = children !== undefined; const searchTargetId = useSettingsSearchTargetId(); useEffect(() => { if (item.kind === "git" && searchTargetId === searchableSetting("git-fetch-interval").id) { setIsExpanded(true); } }, [item.kind, searchTargetId]); return (
{item.label} {version ? {version} : null} {isVcsNotReady(item) ? ( Coming Soon ) : null} {authStatus?.badge ? ( {authStatus.label} ) : null}

{itemSummary({ item, auth, authAccount })}

{hasDetails ? ( ) : null} {!isVcsNotReady(item) ? ( ) : null}
{hasDetails ? (
{children}
) : null}
); } function GitFetchIntervalSettings() { const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); const resolvedBackgroundActivity = resolveServerBackgroundActivitySettings(settings); const automaticGitFetchIntervalSeconds = durationToSeconds( resolvedBackgroundActivity.automaticGitFetchInterval, ); const defaultAutomaticGitFetchIntervalSeconds = durationToSeconds( getBackgroundActivityPresetSettings( getBackgroundActivityBaseProfile(settings.backgroundActivity), ).automaticGitFetchInterval, ); const canResetFetchInterval = automaticGitFetchIntervalSeconds !== defaultAutomaticGitFetchIntervalSeconds; const setting = searchableSetting("git-fetch-interval"); return (
{setting.title} This interval is configured for Git only. The shared Background activity policy still decides whether Git refreshes may run when the timer fires. Custom intervals appear as Advanced in General settings. {canResetFetchInterval ? ( updateSettings( backgroundActivityOverrideSettings(settings.backgroundActivity, { automaticGitFetchInterval: undefined, }), ) } /> ) : null}

Refresh remote branches in the background. Set to 0 to avoid automatic Git prompts.

updateSettings( backgroundActivityOverrideSettings(settings.backgroundActivity, { automaticGitFetchInterval: Duration.seconds(normalizeFetchIntervalSeconds(value)), }), ) } > seconds
); } function SourceControlSectionSkeleton({ title, headerAction, }: { readonly title: string; readonly headerAction?: ReactNode; }) { return ( {SOURCE_CONTROL_SKELETON_ROWS.map((row) => (
))}
); } function EmptySourceControlDiscovery({ error, isPending, onScan, }: { readonly error: string | null; readonly isPending: boolean; readonly onScan: () => void; }) { const hasError = error !== null; return ( {hasError ? "Could not scan the server environment" : "Nothing detected yet"} {hasError ? error : "Install Git on the server, add optional hosting integrations or credentials your workspace needs, then rescan."} ); } export function SourceControlSettingsPanel() { const { environments } = useEnvironments(); const primaryEnvironment = usePrimaryEnvironment(); const fallbackEnvironment = environments.find((environment) => environment.connection.phase === "connected") ?? environments[0] ?? null; const environmentId = primaryEnvironment?.environmentId ?? fallbackEnvironment?.environmentId ?? null; const isPrimaryEnvironment = environmentId === primaryEnvironment?.environmentId; const discovery = useEnvironmentQuery( environmentId === null ? null : sourceControlEnvironment.discovery({ environmentId, input: {}, }), ); const result = discovery.data ?? EMPTY_DISCOVERY_RESULT; const hasVersionControlSystems = result.versionControlSystems.length > 0; const hasDiscoveryItems = hasVersionControlSystems || result.sourceControlProviders.length > 0; const isInitialScanPending = discovery.isPending && discovery.data === null; const handleScan = () => { discovery.refresh(); }; const scanButton = ( } /> Rescan Git and hosting integrations ); return ( {isInitialScanPending ? ( <> ) : hasDiscoveryItems ? ( <> {hasVersionControlSystems ? ( {result.versionControlSystems.map((item) => ( {item.kind === "git" && isPrimaryEnvironment ? ( ) : undefined} ))} ) : null} {result.sourceControlProviders.length > 0 ? ( {result.sourceControlProviders.map((item) => ( ))} ) : null} ) : ( )} {/* Its rows are serverScoped: without a primary they render inert with an explanation, which beats disappearing. */} ); }