"use client"; import { useCallback, useState } from "react"; import { IconBrandGithub, IconCheck, IconExternalLink, IconRefresh, IconTrash, IconX, } from "@tabler/icons-react"; import { Badge } from "@kandev/ui/badge"; import { Button } from "@kandev/ui/button"; import { Spinner } from "@kandev/ui/spinner"; import { SettingsSection } from "@/components/settings/settings-section"; import { useToast } from "@/components/toast-provider"; import { useGitHubStatus } from "@/hooks/domains/github/use-github-status"; import { useGitHubAppRegistrations } from "@/hooks/domains/github/use-github-app-registrations"; import { useTaskGitCredentials, type TaskGitCredentialsState, } from "@/hooks/domains/github/use-task-git-credentials"; import { disconnectGitHubPersonal, disconnectGitHubWorkspace, startGitHubPersonalConnect, } from "@/lib/api/domains/github-api"; import { getGitHubPersonalIdentityState } from "@/lib/github-auth"; import type { GitHubConnectionSource, GitHubConnectionState, GitHubStatus, GitHubAppRegistrationCatalogItem, } from "@/lib/types/github"; import { GitHubConnectionDialog } from "./github-connection-dialog"; import { GitHubAccessHelp } from "./github-access-help"; import { GitHubPermissionsDialog } from "./github-permissions-dialog"; import { GitHubRateLimitDisplay } from "./github-rate-limit"; import { GitHubPRDiscoveryHealthWarning } from "./github-pr-discovery-health"; import { GitHubTaskAccessSummary } from "./github-task-credentials-section"; import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next"; import { controlSizingClassName } from "@kandev/ui/control-sizing"; // Keyed by the wire enum, which is never translated; only the label is copy. // Catalog keys rather than `t()` calls because this is module scope — a `t()` // here would freeze at the boot locale (see docs/i18n.md). const sourceLabelKeys: Record = { pat: "github:personalAccessToken", gh_cli: "github:githubCli", github_app_installation: "github:githubApp", legacy_shared: "github:legacySharedConnection", }; // `t` is threaded in rather than imported at module level so the label follows a // locale switch and stays a pure function of its arguments. function connectionLabel(t: TFunction, status: GitHubStatus): string { const connection = status.automation; if (!connection) return ""; // The first three are GitHub account logins — data, never translated. return ( connection.actor?.login ?? connection.login ?? connection.installation_account_login ?? t("github:githubApp") ); } function automationActor(status: GitHubStatus): string | null { if (!status.authenticated) return null; return status.automation?.actor?.login ?? null; } function StatusLine({ status, onRateLimitOpen, }: { status: GitHubStatus; onRateLimitOpen: () => void; }) { const { t } = useTranslation(); const connection = status.automation; if (!connection) { return (
{t("github:noAutomationConnection")}
); } const actor = automationActor(status); const active = connection.status === "active" && actor !== null; return (
{active ? ( ) : ( )} {actor ?? (connection.status === "active" ? t("github:authenticationUnavailable") : connectionLabel(t, status))} {t(sourceLabelKeys[connection.source])} {connection.status !== "active" && {connection.status}}
); } async function redirectFrom(t: TFunction, start: () => Promise<{ url?: string; URL?: string }>) { const response = await start(); const url = response.url ?? response.URL; // Surfaced to the user through the caller's toast, so it is copy. if (!url) throw new Error(t("github:githubDidNotReturnARedirectUrl")); window.location.assign(url); } function errorMessage(error: unknown, fallback: string) { return error instanceof Error ? error.message : fallback; } function AutomationStatusSummary({ status, app, taskAccess, onRateLimitOpen, }: { status: GitHubStatus; app?: GitHubAppRegistrationCatalogItem; taskAccess: Omit; onRateLimitOpen: () => void; }) { const appAutomation = status.automation?.source === "github_app_installation"; return (
{appAutomation && }
); } function AutomationActorExplanation({ status, appAutomation, }: { status: GitHubStatus; appAutomation: boolean; }) { const { t } = useTranslation(); const actor = status.automation?.actor?.login; if (!status.authenticated || !actor) return null; const humanIdentity = status.effective_personal_actor?.kind === "human"; return (

{appAutomation ? t("github:kandevManagedOperationsUseTheGithub", { actor }) : t("github:kandevManagedOperationsActAs", { actor })}

{!appAutomation && humanIdentity &&

{t("github:thisAccountAlsoPowersMyGithub")}

}
); } function AppRegistrationDetails({ app }: { app?: GitHubAppRegistrationCatalogItem }) { const { t } = useTranslation(); if (!app) return null; return ( <>
{app.display_name} {app.visibility} {t("github:webhook")} {app.webhook_status} {app.source === "managed" ? t("github:createdByKandev") : t("github:imported")}
{app.shared && (

{t("github:thisAppRegistrationIsShared", { count: app.workspace_binding_count })}

)} ); } function AutomationError({ status }: { status: GitHubStatus }) { if (!status.automation?.last_error) return null; return

{status.automation.last_error}

; } function AutomationActions({ status, workspaceId, busy, refreshing, onDisconnect, onRefresh, taskAccess, }: { status: GitHubStatus; workspaceId: string; busy: boolean; refreshing: boolean; onDisconnect: () => void; onRefresh: () => void; taskAccess: TaskGitCredentialsState; }) { const { t } = useTranslation(); return (
{status.automation && ( )}
); } export function GitHubAutomationSettings({ workspaceId }: { workspaceId: string }) { const { t } = useTranslation(); const { status, loaded, loading, refresh } = useGitHubStatus(workspaceId); const appRegistrations = useGitHubAppRegistrations(workspaceId); const taskAccess = useTaskGitCredentials(workspaceId); const [busy, setBusy] = useState(false); const { toast } = useToast(); const disconnect = useCallback(async () => { setBusy(true); try { await disconnectGitHubWorkspace(workspaceId); toast({ description: t("github:workspaceGithubConnectionRemoved"), variant: "success" }); refresh(); } catch (error) { toast({ description: error instanceof Error ? error.message : t("github:disconnectFailed"), variant: "error", }); } finally { setBusy(false); } }, [refresh, toast, workspaceId]); if (!loaded || !status) return ; const activeRegistrationId = status.app_registration?.id ?? status.automation?.app_registration_id; const activeApp = appRegistrations.registrations.find((item) => item.id === activeRegistrationId); return (
); } type PersonalIdentityView = { active: boolean; actor: string; personalActive: boolean; status: GitHubConnectionState | null; }; function personalIdentityView(status: GitHubStatus): PersonalIdentityView { const identity = getGitHubPersonalIdentityState(status); return { active: identity.active, actor: identity.actor, personalActive: identity.personalOAuthActive, status: status.personal?.status ?? null, }; } function PersonalIdentityStatus({ view }: { view: PersonalIdentityView }) { const { t } = useTranslation(); return (
{view.active ? ( ) : ( )} {view.actor} {view.personalActive && {t("github:personalOauth")}} {view.status && view.status !== "active" && ( {view.status} )}
); } function PersonalIdentityActions({ status, busy, onConnect, onDisconnect, }: { status: GitHubStatus; busy: boolean; onConnect: () => void; onDisconnect: () => void; }) { const { t } = useTranslation(); return (
{status.app_available && status.automation?.source === "github_app_installation" && ( )} {status.personal && ( )}
); } export function GitHubPersonalSettings({ workspaceId }: { workspaceId: string }) { const { t } = useTranslation(); const { status, loaded, refresh } = useGitHubStatus(workspaceId); const [busy, setBusy] = useState(false); const { toast } = useToast(); if (!loaded || !status) return null; const view = personalIdentityView(status); const appAutomation = status.automation?.source === "github_app_installation"; if (!status.automation) return null; if (!appAutomation) return null; const disconnect = async () => { setBusy(true); try { await disconnectGitHubPersonal(workspaceId); toast({ description: t("github:personalGithubIdentityDisconnected"), variant: "success" }); refresh(); } catch (error) { toast({ description: error instanceof Error ? error.message : t("github:disconnectFailed"), variant: "error", }); } finally { setBusy(false); } }; const connect = () => { void redirectFrom(t, () => startGitHubPersonalConnect(workspaceId)).catch((error: unknown) => toast({ description: errorMessage(error, t("github:identityConnectionFailed")), variant: "error", }), ); }; return (
{status.personal?.last_error && (

{status.personal.last_error}

)}
); } function LoadingStatus() { const { t } = useTranslation(); return (
{t("github:checkingGithubConnection")}
); } /** Compatibility export used by older settings entrypoints. */ export function GitHubStatusCard({ workspaceId }: { workspaceId: string }) { return ; }