diff --git a/apps/code/src/renderer/features/onboarding/components/CliCheckPanel.tsx b/apps/code/src/renderer/features/onboarding/components/CliCheckPanel.tsx
new file mode 100644
index 000000000..9da6107a5
--- /dev/null
+++ b/apps/code/src/renderer/features/onboarding/components/CliCheckPanel.tsx
@@ -0,0 +1,58 @@
+import { CheckCircle, CircleNotch } from "@phosphor-icons/react";
+import { Box, Flex, Text } from "@radix-ui/themes";
+import type { ReactNode } from "react";
+import { PANEL_SHADOW } from "./onboardingStyles";
+
+interface CliCheckPanelProps {
+ icon: ReactNode;
+ title: string;
+ isLoading: boolean;
+ statusBadge: ReactNode | null;
+ children?: ReactNode;
+}
+
+export function CliCheckPanel({
+ icon,
+ title,
+ isLoading,
+ statusBadge,
+ children,
+}: CliCheckPanelProps) {
+ return (
+
+
+
+
+ {icon}
+
+ {title}
+
+
+ {isLoading ? (
+
+ ) : (
+ statusBadge
+ )}
+
+ {children}
+
+
+ );
+}
+
+interface InstalledBadgeProps {
+ label: string;
+}
+
+export function InstalledBadge({ label }: InstalledBadgeProps) {
+ return (
+
+
+ {label}
+
+ );
+}
diff --git a/apps/code/src/renderer/features/onboarding/components/CliInstallStep.tsx b/apps/code/src/renderer/features/onboarding/components/CliInstallStep.tsx
deleted file mode 100644
index 718876d31..000000000
--- a/apps/code/src/renderer/features/onboarding/components/CliInstallStep.tsx
+++ /dev/null
@@ -1,372 +0,0 @@
-import { Tooltip } from "@components/ui/Tooltip";
-import {
- ArrowLeft,
- ArrowRight,
- ArrowSquareOut,
- ArrowsClockwise,
- Check,
- CheckCircle,
- CircleNotch,
- Copy,
- GitBranch,
- GithubLogo,
- Warning,
-} from "@phosphor-icons/react";
-import { Box, Button, Flex, IconButton, Text } from "@radix-ui/themes";
-import builderHog from "@renderer/assets/images/hedgehogs/builder-hog-03.png";
-import { trpcClient, useTRPC } from "@renderer/trpc/client";
-import { ANALYTICS_EVENTS } from "@shared/types/analytics";
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { track } from "@utils/analytics";
-import { EXTERNAL_LINKS } from "@utils/links";
-import { motion } from "framer-motion";
-import { useCallback, useEffect, useRef, useState } from "react";
-import { OnboardingHogTip } from "./OnboardingHogTip";
-import { StepActions } from "./StepActions";
-
-function CommandLine({ command }: { command: string }) {
- const [copied, setCopied] = useState(false);
-
- const handleCopy = useCallback(async () => {
- await navigator.clipboard.writeText(command);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- }, [command]);
-
- return (
-
-
-
- $
-
-
- {command}
-
-
-
- void handleCopy()}
- aria-label="Copy command"
- >
- {copied ? : }
-
-
-
- );
-}
-
-interface CliInstallStepProps {
- onComplete: (cliSkipped: boolean) => void;
- onBack: () => void;
-}
-
-export function CliInstallStep({ onComplete, onBack }: CliInstallStepProps) {
- const trpc = useTRPC();
- const queryClient = useQueryClient();
- const [isCheckingGit, setIsCheckingGit] = useState(false);
- const [isCheckingGh, setIsCheckingGh] = useState(false);
-
- const { data: gitStatus, isLoading: isLoadingGit } = useQuery(
- trpc.git.getGitStatus.queryOptions(undefined, { staleTime: 30_000 }),
- );
- const { data: ghStatus, isLoading: isLoadingGh } = useQuery(
- trpc.git.getGhStatus.queryOptions(undefined, { staleTime: 30_000 }),
- );
-
- const gitInstalled = gitStatus?.installed ?? false;
- const ghInstalled = ghStatus?.installed ?? false;
- const ghAuthenticated = ghStatus?.authenticated ?? false;
- const allReady = gitInstalled && ghInstalled && ghAuthenticated;
-
- const checkFiredRef = useRef(false);
- useEffect(() => {
- if (checkFiredRef.current || isLoadingGit || isLoadingGh) return;
- checkFiredRef.current = true;
- track(ANALYTICS_EVENTS.ONBOARDING_CLI_CHECK_COMPLETED, {
- git_installed: gitInstalled,
- gh_installed: ghInstalled,
- gh_authenticated: ghAuthenticated,
- });
- }, [isLoadingGit, isLoadingGh, gitInstalled, ghInstalled, ghAuthenticated]);
-
- const handleCheckGit = useCallback(async () => {
- setIsCheckingGit(true);
- await queryClient.invalidateQueries(trpc.git.getGitStatus.queryFilter());
- setIsCheckingGit(false);
- }, [queryClient, trpc]);
-
- const handleCheckGh = useCallback(async () => {
- setIsCheckingGh(true);
- await queryClient.invalidateQueries(trpc.git.getGhStatus.queryFilter());
- setIsCheckingGh(false);
- }, [queryClient, trpc]);
-
- return (
-
-
-
-
-
-
-
-
- Install required tools
-
-
- These CLI tools are needed for code management and GitHub
- workflows.
-
-
-
-
- {/* Git box */}
-
-
-
-
-
-
-
- Git
-
-
- {isLoadingGit && (
-
- )}
- {!isLoadingGit && gitInstalled && (
-
-
-
- Installed
- {gitStatus?.version
- ? ` (${gitStatus.version})`
- : ""}
-
-
- )}
-
- {!isLoadingGit && !gitInstalled && (
-
-
- Install with Homebrew or Xcode Command Line Tools:
-
-
-
-
-
-
-
-
-
-
- )}
-
-
-
-
- {/* GitHub CLI box */}
-
-
-
-
-
-
-
- GitHub CLI
-
-
- {isLoadingGh && (
-
- )}
- {!isLoadingGh && ghInstalled && ghAuthenticated && (
-
-
-
- {ghStatus?.username
- ? `Logged in as ${ghStatus.username}`
- : "Authenticated"}
-
-
- )}
- {!isLoadingGh && ghInstalled && !ghAuthenticated && (
-
-
-
- Not logged in
-
-
- )}
-
- {!isLoadingGh && !ghInstalled && (
-
-
- Install with Homebrew:
-
-
-
-
-
-
-
- )}
- {!isLoadingGh && ghInstalled && !ghAuthenticated && (
-
-
- Run this in your terminal to log in:
-
-
-
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
- {allReady ? (
-
- ) : (
-
- )}
-
-
-
- );
-}
diff --git a/apps/code/src/renderer/features/onboarding/components/ConnectGitHubStep.tsx b/apps/code/src/renderer/features/onboarding/components/ConnectGitHubStep.tsx
new file mode 100644
index 000000000..43bbee7a5
--- /dev/null
+++ b/apps/code/src/renderer/features/onboarding/components/ConnectGitHubStep.tsx
@@ -0,0 +1,122 @@
+import { useUserGithubIntegrations } from "@hooks/useIntegrations";
+import {
+ ArrowLeft,
+ ArrowRight,
+ CheckCircle,
+ Cloud,
+ GitPullRequest,
+} from "@phosphor-icons/react";
+import { Button, Flex, Text } from "@radix-ui/themes";
+import builderHog from "@renderer/assets/images/hedgehogs/builder-hog-03.png";
+import type { OnboardingStepCompletedProperties } from "@shared/types/analytics";
+import { motion } from "framer-motion";
+import { GitHubConnectPanel } from "./GitHubConnectPanel";
+import { OnboardingHogTip } from "./OnboardingHogTip";
+import { OptionalBadge } from "./OptionalBadge";
+import { StepActions } from "./StepActions";
+
+type StepContext = Pick;
+
+interface ConnectGitHubStepProps {
+ onNext: (context?: StepContext) => void;
+ onBack: () => void;
+}
+
+export function ConnectGitHubStep({ onNext, onBack }: ConnectGitHubStepProps) {
+ const { data: githubUserIntegrations = [] } = useUserGithubIntegrations();
+ const handleContinue = () => {
+ onNext({ github_connected: githubUserIntegrations.length > 0 });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ Connect GitHub
+
+
+
+
+ Unlocks the parts of PostHog Code that leave your machine.
+
+
+
+
+
+
+
+
+
+ Run tasks in cloud sandboxes instead of your machine.
+
+
+
+
+
+ Push branches and open pull requests from agents.
+
+
+
+
+
+ Review PR comments and reply to threads from inside the
+ app.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx b/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx
new file mode 100644
index 000000000..08119f7a6
--- /dev/null
+++ b/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx
@@ -0,0 +1,531 @@
+import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient";
+import { useAuthStateValue } from "@features/auth/hooks/authQueries";
+import {
+ describeGithubConnectError,
+ invalidateGithubQueries,
+ useGithubConnect,
+} from "@features/integrations/hooks/useGithubUserConnect";
+import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore";
+import {
+ useUserGithubIntegrations,
+ useUserRepositoryIntegration,
+} from "@hooks/useIntegrations";
+import {
+ ArrowSquareOut,
+ ArrowsClockwise,
+ CheckCircle,
+ GearSix,
+ GithubLogo,
+ Plus,
+} from "@phosphor-icons/react";
+import {
+ AlertDialog,
+ Box,
+ Button,
+ DropdownMenu,
+ Flex,
+ Skeleton,
+ Spinner,
+ Text,
+} from "@radix-ui/themes";
+import { trpcClient } from "@renderer/trpc/client";
+import {
+ ANALYTICS_EVENTS,
+ type OnboardingGithubConnectFlow,
+} from "@shared/types/analytics";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { track } from "@utils/analytics";
+import { useEffect, useMemo, useRef, useState } from "react";
+import { toast } from "sonner";
+import { useProjectsWithIntegrations } from "../hooks/useProjectsWithIntegrations";
+import { OptionalBadge } from "./OptionalBadge";
+import { PANEL_SHADOW } from "./onboardingStyles";
+
+function getPanelMessage(opts: {
+ hasConnectError: boolean;
+ connectError: Parameters[0];
+ timedOut: boolean;
+ isConnecting: boolean;
+}): string {
+ if (opts.hasConnectError)
+ return describeGithubConnectError(opts.connectError);
+ if (opts.timedOut) {
+ return "We didn't hear back from GitHub. If the browser tab was closed, click Connect again.";
+ }
+ if (opts.isConnecting) return "Waiting for GitHub...";
+ return "Unlocks cloud runs, branch pushes, and PR review on this account.";
+}
+
+export function GitHubConnectPanel() {
+ const queryClient = useQueryClient();
+ const currentProjectId = useAuthStateValue((state) => state.projectId);
+ const { projects, projectsWithGithub, isLoading } =
+ useProjectsWithIntegrations();
+ const manuallySelectedProjectId = useOnboardingStore(
+ (state) => state.selectedProjectId,
+ );
+ const setSelectedProjectId = useOnboardingStore(
+ (state) => state.selectProjectId,
+ );
+ const selectedProjectId = useMemo(() => {
+ if (manuallySelectedProjectId !== null) return manuallySelectedProjectId;
+ return currentProjectId ?? projects[0]?.id ?? null;
+ }, [manuallySelectedProjectId, currentProjectId, projects]);
+ const selectedProject = useMemo(
+ () => projects.find((p) => p.id === selectedProjectId),
+ [projects, selectedProjectId],
+ );
+
+ const {
+ error: connectError,
+ isConnecting,
+ isTimedOut: timedOut,
+ hasError: hasConnectError,
+ connect: handleConnectGitHub,
+ reset: resetConnect,
+ } = useGithubConnect({
+ projectId: selectedProjectId,
+ projectHasTeamIntegration: selectedProject?.hasGithubIntegration ?? null,
+ onConnected: () => track(ANALYTICS_EVENTS.ONBOARDING_GITHUB_CONNECTED),
+ });
+ const canTakeAction = !isConnecting && !timedOut && !hasConnectError;
+
+ const initiateConnect = (
+ flowType: OnboardingGithubConnectFlow,
+ isRetry = false,
+ ) => {
+ track(ANALYTICS_EVENTS.ONBOARDING_GITHUB_CONNECT_STARTED, {
+ flow_type: flowType,
+ is_retry: isRetry,
+ });
+ void handleConnectGitHub();
+ };
+
+ const failureFingerprintRef = useRef(null);
+ useEffect(() => {
+ if (!hasConnectError && !timedOut) {
+ failureFingerprintRef.current = null;
+ return;
+ }
+ const fingerprint = timedOut ? "timeout" : (connectError?.code ?? "error");
+ if (failureFingerprintRef.current === fingerprint) return;
+ failureFingerprintRef.current = fingerprint;
+ track(ANALYTICS_EVENTS.ONBOARDING_GITHUB_CONNECT_FAILED, {
+ reason: timedOut ? "timeout" : "error",
+ error_type: connectError?.code ?? undefined,
+ });
+ }, [hasConnectError, timedOut, connectError]);
+
+ const defaultPanelMessage = getPanelMessage({
+ hasConnectError,
+ connectError,
+ timedOut,
+ isConnecting,
+ });
+
+ const {
+ data: githubUserIntegrations = [],
+ isLoading: githubUserIntegrationsLoading,
+ } = useUserGithubIntegrations();
+ const hasGitIntegration = githubUserIntegrations.length > 0;
+ const { failedInstallationIds, reposByInstallationId } =
+ useUserRepositoryIntegration();
+ const anyIntegrationStale = githubUserIntegrations.some((i) =>
+ failedInstallationIds.includes(i.installation_id),
+ );
+
+ const alternativeConnectedProjects = useMemo(() => {
+ if (hasGitIntegration) return [];
+ if (!projectsWithGithub.length) return [];
+ return projectsWithGithub.filter((p) => p.id !== selectedProjectId);
+ }, [hasGitIntegration, projectsWithGithub, selectedProjectId]);
+ const [selectedAlternativeId, setSelectedAlternativeId] = useState<
+ number | null
+ >(null);
+ const selectedAlternative = useMemo(() => {
+ if (!alternativeConnectedProjects.length) return null;
+ return (
+ alternativeConnectedProjects.find(
+ (p) => p.id === selectedAlternativeId,
+ ) ?? alternativeConnectedProjects[0]
+ );
+ }, [alternativeConnectedProjects, selectedAlternativeId]);
+
+ const apiClient = useOptionalAuthenticatedClient();
+ const [disconnectTarget, setDisconnectTarget] = useState<{
+ installationId: string;
+ accountName: string;
+ } | null>(null);
+ const [reconnectingInstallationId, setReconnectingInstallationId] = useState<
+ string | null
+ >(null);
+ const disconnectMutation = useMutation({
+ mutationFn: async (opts: { installationId: string; silent?: boolean }) => {
+ if (!apiClient) throw new Error("Not authenticated");
+ await apiClient.disconnectGithubUserIntegration(opts.installationId);
+ return { silent: opts.silent ?? false };
+ },
+ onSuccess: ({ silent }) => {
+ setDisconnectTarget(null);
+ invalidateGithubQueries(queryClient, selectedProjectId);
+ if (!silent) toast.success("GitHub disconnected.");
+ },
+ onError: (e) => {
+ toast.error(
+ e instanceof Error ? e.message : "Failed to disconnect GitHub.",
+ );
+ },
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+
+ Connect GitHub
+
+
+ {isLoading || githubUserIntegrationsLoading ? (
+
+ ) : hasGitIntegration ? (
+ anyIntegrationStale ? (
+
+ Reconnect needed
+
+ ) : (
+
+
+
+ {githubUserIntegrations.length > 1
+ ? `Connected (${githubUserIntegrations.length})`
+ : "Connected"}
+
+
+ )
+ ) : (
+
+ )}
+
+ {!hasGitIntegration &&
+ !isLoading &&
+ !githubUserIntegrationsLoading &&
+ (selectedProject?.hasGithubIntegration && canTakeAction ? (
+
+ GitHub is already set up on{" "}
+ {selectedProject.name}.
+ Sign in with one click to link your account, no admin approval
+ needed.
+
+ ) : selectedAlternative && selectedProject && canTakeAction ? (
+
+ GitHub is already connected on{" "}
+ {alternativeConnectedProjects.length > 1 ? (
+
+
+
+
+
+ {alternativeConnectedProjects.map((p) => (
+ setSelectedAlternativeId(p.id)}
+ >
+ {p.name}
+
+ {p.organization.name}
+
+
+ ))}
+
+
+ ) : (
+ <>
+
+ {selectedAlternative.name}
+ {" "}
+ ({selectedAlternative.organization.name})
+ >
+ )}
+ .
+
+ ) : (
+
+ {defaultPanelMessage}
+
+ ))}
+
+ {hasGitIntegration ? (
+
+ {githubUserIntegrations.map((integration) => {
+ const installationId = integration.installation_id;
+ const accountName = integration.account?.name ?? "GitHub";
+ const installRepos = reposByInstallationId[installationId];
+ const isLoadingInstallRepos = installRepos === undefined;
+ const isStale = failedInstallationIds.includes(installationId);
+ const isReconnecting =
+ reconnectingInstallationId === installationId;
+ return (
+
+
+
+
+ {accountName}
+
+
+ {integration.account?.type === "Organization"
+ ? "org"
+ : "personal"}
+
+
+ {isStale ? (
+
+ Reconnect needed
+
+ ) : (
+
+ {isLoadingInstallRepos
+ ? "Loading…"
+ : installRepos.length === 1
+ ? "1 repo"
+ : `${installRepos.length} repos`}
+
+ )}
+
+
+ {isStale && (
+
+ )}
+
+
+
+
+ );
+ })}
+
+
+
+
+
+ ) : !isLoading && !githubUserIntegrationsLoading ? (
+ selectedProject?.hasGithubIntegration && canTakeAction ? (
+
+ ) : selectedAlternative && selectedProject && canTakeAction ? (
+
+
+
+
+ ) : (
+
+
+ {hasConnectError && (
+
+ )}
+
+ )
+ ) : null}
+
+
+
+ {
+ if (!next && !disconnectMutation.isPending) {
+ setDisconnectTarget(null);
+ }
+ }}
+ >
+
+
+ Disconnect{" "}
+ {disconnectTarget ? disconnectTarget.accountName : "GitHub"}
+
+
+ This removes your personal GitHub authorization from PostHog. You
+ can reconnect at any time. The GitHub App itself stays installed in
+ your org — uninstall it on GitHub if you want to remove that too.
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx b/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx
deleted file mode 100644
index 3fd3b060c..000000000
--- a/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx
+++ /dev/null
@@ -1,755 +0,0 @@
-import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient";
-import { useSelectProjectMutation } from "@features/auth/hooks/authMutations";
-import { useAuthStateValue } from "@features/auth/hooks/authQueries";
-import { FolderPicker } from "@features/folder-picker/components/FolderPicker";
-import {
- describeGithubConnectError,
- invalidateGithubQueries,
- useGithubConnect,
-} from "@features/integrations/hooks/useGithubUserConnect";
-import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore";
-import {
- useUserGithubIntegrations,
- useUserRepositoryIntegration,
-} from "@hooks/useIntegrations";
-import {
- ArrowLeft,
- ArrowRight,
- ArrowSquareOut,
- ArrowsClockwise,
- CheckCircle,
- CircleNotch,
- FolderOpen,
- GearSix,
- GitBranch,
- Plus,
-} from "@phosphor-icons/react";
-import { cn } from "@posthog/quill";
-import {
- AlertDialog,
- Box,
- Button,
- DropdownMenu,
- Flex,
- Skeleton,
- Spinner,
- Text,
-} from "@radix-ui/themes";
-import builderHog from "@renderer/assets/images/hedgehogs/builder-hog-03.png";
-import { trpcClient } from "@renderer/trpc/client";
-import { ANALYTICS_EVENTS } from "@shared/types/analytics";
-import { useMutation, useQueryClient } from "@tanstack/react-query";
-import { track } from "@utils/analytics";
-import { AnimatePresence, motion } from "framer-motion";
-import { useMemo, useState } from "react";
-import { toast } from "sonner";
-import type { DetectedRepo } from "../hooks/useOnboardingFlow";
-import { useProjectsWithIntegrations } from "../hooks/useProjectsWithIntegrations";
-import { OnboardingHogTip } from "./OnboardingHogTip";
-import { StepActions } from "./StepActions";
-
-const PANEL_SHADOW = "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)";
-
-function getPanelMessage(opts: {
- hasConnectError: boolean;
- connectError: Parameters[0];
- timedOut: boolean;
- isConnecting: boolean;
-}): string {
- if (opts.hasConnectError)
- return describeGithubConnectError(opts.connectError);
- if (opts.timedOut) {
- return "We didn't hear back from GitHub. If the browser tab was closed, click Connect again.";
- }
- if (opts.isConnecting) return "Waiting for GitHub...";
- return "Optional. Lets cloud agents work on this repo and open pull requests for you.";
-}
-
-interface GitIntegrationStepProps {
- onNext: () => void;
- onBack: () => void;
- selectedDirectory: string;
- detectedRepo: DetectedRepo | null;
- isDetectingRepo: boolean;
- onDirectoryChange: (path: string) => void;
-}
-
-export function GitIntegrationStep({
- onNext,
- onBack,
- selectedDirectory,
- detectedRepo,
- isDetectingRepo,
- onDirectoryChange,
-}: GitIntegrationStepProps) {
- const currentProjectId = useAuthStateValue((state) => state.projectId);
- const selectProjectMutation = useSelectProjectMutation();
-
- const queryClient = useQueryClient();
- const { projects, projectsWithGithub, isLoading } =
- useProjectsWithIntegrations();
-
- const manuallySelectedProjectId = useOnboardingStore(
- (state) => state.selectedProjectId,
- );
- const setSelectedProjectId = useOnboardingStore(
- (state) => state.selectProjectId,
- );
-
- const selectedProjectId = useMemo(() => {
- if (manuallySelectedProjectId !== null) {
- return manuallySelectedProjectId;
- }
- return currentProjectId ?? projects[0]?.id ?? null;
- }, [manuallySelectedProjectId, currentProjectId, projects]);
-
- const selectedProject = useMemo(
- () => projects.find((p) => p.id === selectedProjectId),
- [projects, selectedProjectId],
- );
-
- const {
- error: connectError,
- isConnecting,
- isTimedOut: timedOut,
- hasError: hasConnectError,
- connect: handleConnectGitHub,
- reset: resetConnect,
- } = useGithubConnect({
- projectId: selectedProjectId,
- projectHasTeamIntegration: selectedProject?.hasGithubIntegration ?? null,
- onConnected: () => track(ANALYTICS_EVENTS.ONBOARDING_GITHUB_CONNECTED),
- });
- const canTakeAction = !isConnecting && !timedOut && !hasConnectError;
- const defaultPanelMessage = getPanelMessage({
- hasConnectError,
- connectError,
- timedOut,
- isConnecting,
- });
-
- const {
- data: githubUserIntegrations = [],
- isLoading: githubUserIntegrationsLoading,
- } = useUserGithubIntegrations();
- const hasGitIntegration = githubUserIntegrations.length > 0;
- const { repositories, failedInstallationIds, reposByInstallationId } =
- useUserRepositoryIntegration();
- const anyIntegrationStale = githubUserIntegrations.some((i) =>
- failedInstallationIds.includes(i.installation_id),
- );
-
- const alternativeConnectedProjects = useMemo(() => {
- if (hasGitIntegration) return [];
- if (!projectsWithGithub.length) return [];
- return projectsWithGithub.filter((p) => p.id !== selectedProjectId);
- }, [hasGitIntegration, projectsWithGithub, selectedProjectId]);
-
- const [selectedAlternativeId, setSelectedAlternativeId] = useState<
- number | null
- >(null);
-
- const selectedAlternative = useMemo(() => {
- if (!alternativeConnectedProjects.length) return null;
- return (
- alternativeConnectedProjects.find(
- (p) => p.id === selectedAlternativeId,
- ) ?? alternativeConnectedProjects[0]
- );
- }, [alternativeConnectedProjects, selectedAlternativeId]);
-
- const repoMatchesGitHub = useMemo(() => {
- if (!detectedRepo || repositories.length === 0) return false;
- return repositories.some(
- (r) => r.toLowerCase() === detectedRepo.fullName.toLowerCase(),
- );
- }, [detectedRepo, repositories]);
-
- const apiClient = useOptionalAuthenticatedClient();
- const [disconnectTarget, setDisconnectTarget] = useState<{
- installationId: string;
- accountName: string;
- } | null>(null);
- const [reconnectingInstallationId, setReconnectingInstallationId] = useState<
- string | null
- >(null);
- const disconnectMutation = useMutation({
- mutationFn: async (opts: { installationId: string; silent?: boolean }) => {
- if (!apiClient) {
- throw new Error("Not authenticated");
- }
- await apiClient.disconnectGithubUserIntegration(opts.installationId);
- return { silent: opts.silent ?? false };
- },
- onSuccess: ({ silent }) => {
- setDisconnectTarget(null);
- invalidateGithubQueries(queryClient, selectedProjectId);
- if (!silent) toast.success("GitHub disconnected.");
- },
- onError: (e) => {
- toast.error(
- e instanceof Error ? e.message : "Failed to disconnect GitHub.",
- );
- },
- });
-
- const handleContinue = () => {
- if (selectedProjectId && selectedProjectId !== currentProjectId) {
- selectProjectMutation.mutate(selectedProjectId);
- }
- onNext();
- };
-
- return (
-
-
-
-
- {/* Header + content */}
-
-
-
-
- Give your agents access to code
-
-
- Pick a repository to run local tasks on this machine.
- Connect GitHub to send tasks to cloud agents.
-
-
-
-
- {/* Local folder picker */}
-
-
-
-
-
-
-
- Choose your repository
-
-
-
- Select a single repository folder, not a parent folder
- that contains multiple repos.
-
-
-
-
- {isDetectingRepo && (
-
-
-
-
- Detecting repository...
-
-
-
- )}
- {!isDetectingRepo &&
- selectedDirectory &&
- detectedRepo && (
-
-
-
-
- {repoMatchesGitHub
- ? `Linked to ${detectedRepo.fullName} on GitHub`
- : `Detected ${detectedRepo.fullName}`}
-
-
-
- )}
- {!isDetectingRepo &&
- selectedDirectory &&
- !detectedRepo && (
-
-
- No git remote detected. You can still continue.
-
-
- )}
-
-
-
-
-
- {/* GitHub integration */}
- {selectedDirectory && (
-
-
-
-
-
-
-
-
- Connect GitHub
-
-
- {isLoading || githubUserIntegrationsLoading ? (
-
- ) : hasGitIntegration ? (
- anyIntegrationStale ? (
-
- Reconnect needed
-
- ) : (
-
-
-
- {githubUserIntegrations.length > 1
- ? `Connected (${githubUserIntegrations.length})`
- : "Connected"}
-
-
- )
- ) : (
-
- Optional
-
- )}
-
- {!hasGitIntegration &&
- !isLoading &&
- !githubUserIntegrationsLoading &&
- (selectedProject?.hasGithubIntegration &&
- canTakeAction ? (
-
- GitHub is already set up on{" "}
-
- {selectedProject.name}
-
- . Sign in with one click to link your account, no
- admin approval needed.
-
- ) : selectedAlternative &&
- selectedProject &&
- canTakeAction ? (
-
- GitHub is already connected on{" "}
- {alternativeConnectedProjects.length > 1 ? (
-
-
-
-
-
- {alternativeConnectedProjects.map((p) => (
-
- setSelectedAlternativeId(p.id)
- }
- >
-
- {p.name}
-
-
- {p.organization.name}
-
-
- ))}
-
-
- ) : (
- <>
-
- {selectedAlternative.name}
- {" "}
- ({selectedAlternative.organization.name})
- >
- )}
- .
-
- ) : (
-
- {defaultPanelMessage}
-
- ))}
-
- {hasGitIntegration ? (
-
- {githubUserIntegrations.map((integration) => {
- const installationId = integration.installation_id;
- const accountName =
- integration.account?.name ?? "GitHub";
- const installRepos =
- reposByInstallationId[installationId];
- const isLoadingInstallRepos =
- installRepos === undefined;
- const isStale =
- failedInstallationIds.includes(installationId);
- const isReconnecting =
- reconnectingInstallationId === installationId;
- return (
-
-
-
-
- {accountName}
-
-
- {integration.account?.type ===
- "Organization"
- ? "org"
- : "personal"}
-
-
- {isStale ? (
-
- Reconnect needed
-
- ) : (
-
- {isLoadingInstallRepos
- ? "Loading…"
- : installRepos.length === 1
- ? "1 repo"
- : `${installRepos.length} repos`}
-
- )}
-
-
- {isStale && (
-
- )}
-
-
-
-
- );
- })}
-
-
-
-
-
- ) : !isLoading && !githubUserIntegrationsLoading ? (
- selectedProject?.hasGithubIntegration &&
- canTakeAction ? (
-
- ) : selectedAlternative &&
- selectedProject &&
- canTakeAction ? (
-
-
-
-
- ) : (
-
-
- {hasConnectError && (
-
- )}
-
- )
- ) : null}
-
-
-
- )}
-
-
- {/* Hog tip */}
-
-
-
-
-
-
-
-
-
- {
- if (!next && !disconnectMutation.isPending) {
- setDisconnectTarget(null);
- }
- }}
- >
-
-
- Disconnect{" "}
- {disconnectTarget ? disconnectTarget.accountName : "GitHub"}
-
-
- This removes your personal GitHub authorization from PostHog. You
- can reconnect at any time. The GitHub App itself stays installed
- in your org — uninstall it on GitHub if you want to remove that
- too.
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/code/src/renderer/features/onboarding/components/InstallCliStep.tsx b/apps/code/src/renderer/features/onboarding/components/InstallCliStep.tsx
new file mode 100644
index 000000000..d988d2701
--- /dev/null
+++ b/apps/code/src/renderer/features/onboarding/components/InstallCliStep.tsx
@@ -0,0 +1,326 @@
+import { Tooltip } from "@components/ui/Tooltip";
+import {
+ ArrowLeft,
+ ArrowRight,
+ ArrowSquareOut,
+ ArrowsClockwise,
+ Check,
+ Copy,
+ GitBranch,
+ GithubLogo,
+ Warning,
+} from "@phosphor-icons/react";
+import { Button, Flex, IconButton, Text } from "@radix-ui/themes";
+import builderHog from "@renderer/assets/images/hedgehogs/builder-hog-03.png";
+import { trpcClient, useTRPC } from "@renderer/trpc/client";
+import {
+ ANALYTICS_EVENTS,
+ type OnboardingStepCompletedProperties,
+} from "@shared/types/analytics";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { track } from "@utils/analytics";
+import { EXTERNAL_LINKS } from "@utils/links";
+import { motion } from "framer-motion";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { CliCheckPanel, InstalledBadge } from "./CliCheckPanel";
+import { OnboardingHogTip } from "./OnboardingHogTip";
+import { OptionalBadge } from "./OptionalBadge";
+import { StepActions } from "./StepActions";
+
+function CommandLine({ command }: { command: string }) {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = useCallback(async () => {
+ await navigator.clipboard.writeText(command);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }, [command]);
+
+ return (
+
+
+
+ $
+
+
+ {command}
+
+
+
+ void handleCopy()}
+ aria-label="Copy command"
+ >
+ {copied ? : }
+
+
+
+ );
+}
+
+type StepContext = Pick<
+ OnboardingStepCompletedProperties,
+ "git_installed" | "gh_installed" | "gh_authenticated"
+>;
+
+interface InstallCliStepProps {
+ onNext: (context?: StepContext) => void;
+ onBack: () => void;
+}
+
+export function InstallCliStep({ onNext, onBack }: InstallCliStepProps) {
+ const trpc = useTRPC();
+ const queryClient = useQueryClient();
+
+ const [isCheckingGit, setIsCheckingGit] = useState(false);
+ const [isCheckingGh, setIsCheckingGh] = useState(false);
+ const { data: gitStatus, isLoading: isLoadingGit } = useQuery(
+ trpc.git.getGitStatus.queryOptions(undefined, { staleTime: 30_000 }),
+ );
+ const { data: ghStatus, isLoading: isLoadingGh } = useQuery(
+ trpc.git.getGhStatus.queryOptions(undefined, { staleTime: 30_000 }),
+ );
+ const gitInstalled = gitStatus?.installed ?? false;
+ const ghInstalled = ghStatus?.installed ?? false;
+ const ghAuthenticated = ghStatus?.authenticated ?? false;
+
+ const checkFiredRef = useRef(false);
+ useEffect(() => {
+ if (checkFiredRef.current) return;
+ if (gitStatus === undefined || ghStatus === undefined) return;
+ checkFiredRef.current = true;
+ track(ANALYTICS_EVENTS.ONBOARDING_CLI_CHECK_COMPLETED, {
+ git_installed: gitInstalled,
+ gh_installed: ghInstalled,
+ gh_authenticated: ghAuthenticated,
+ });
+ }, [gitStatus, ghStatus, gitInstalled, ghInstalled, ghAuthenticated]);
+
+ const handleCheckGit = useCallback(async () => {
+ setIsCheckingGit(true);
+ await queryClient.invalidateQueries(trpc.git.getGitStatus.queryFilter());
+ setIsCheckingGit(false);
+ }, [queryClient, trpc]);
+
+ const handleCheckGh = useCallback(async () => {
+ setIsCheckingGh(true);
+ await queryClient.invalidateQueries(trpc.git.getGhStatus.queryFilter());
+ setIsCheckingGh(false);
+ }, [queryClient, trpc]);
+
+ const handleContinue = () => {
+ onNext({
+ git_installed: gitInstalled,
+ gh_installed: ghInstalled,
+ gh_authenticated: ghAuthenticated,
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ Install CLI tools
+
+
+
+
+ Agents use these to manage branches and open pull requests
+ on your behalf.
+
+
+
+
+
+ }
+ title="Git"
+ isLoading={isLoadingGit}
+ statusBadge={
+ gitInstalled ? (
+
+ ) : null
+ }
+ >
+ {!isLoadingGit && !gitInstalled && (
+
+
+ Install with Homebrew or Xcode Command Line Tools:
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+ }
+ title="GitHub CLI"
+ isLoading={isLoadingGh}
+ statusBadge={
+ ghInstalled && ghAuthenticated ? (
+
+ ) : ghInstalled ? (
+
+
+
+ Not logged in
+
+
+ ) : null
+ }
+ >
+ {!isLoadingGh && !ghInstalled && (
+
+
+ Install with Homebrew:
+
+
+
+
+
+
+
+ )}
+ {!isLoadingGh && ghInstalled && !ghAuthenticated && (
+
+
+ Run this in your terminal to log in:
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx b/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx
index 985f5826f..33949aa4d 100644
--- a/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx
+++ b/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx
@@ -6,7 +6,10 @@ import { useUserGithubIntegrations } from "@hooks/useIntegrations";
import { ArrowRight, SignOut } from "@phosphor-icons/react";
import { Button, Flex } from "@radix-ui/themes";
import { IS_DEV } from "@shared/constants/environment";
-import { ANALYTICS_EVENTS } from "@shared/types/analytics";
+import {
+ ANALYTICS_EVENTS,
+ type OnboardingStepCompletedProperties,
+} from "@shared/types/analytics";
import { useNavigationStore } from "@stores/navigationStore";
import { track } from "@utils/analytics";
import { AnimatePresence, LayoutGroup, motion } from "framer-motion";
@@ -14,10 +17,11 @@ import { useEffect, useRef } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { useOnboardingFlow } from "../hooks/useOnboardingFlow";
-import { CliInstallStep } from "./CliInstallStep";
-import { GitIntegrationStep } from "./GitIntegrationStep";
+import { ConnectGitHubStep } from "./ConnectGitHubStep";
+import { InstallCliStep } from "./InstallCliStep";
import { InviteCodeStep } from "./InviteCodeStep";
import { ProjectSelectStep } from "./ProjectSelectStep";
+import { SelectRepoStep } from "./SelectRepoStep";
import { StepIndicator } from "./StepIndicator";
import { WelcomeScreen } from "./WelcomeScreen";
@@ -79,7 +83,12 @@ export function OnboardingFlow() {
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [currentStep]);
- const trackStepCompleted = () => {
+ type StepContext = Omit<
+ OnboardingStepCompletedProperties,
+ "step_id" | "step_index" | "total_steps" | "duration_seconds"
+ >;
+
+ const trackStepCompleted = (context?: StepContext) => {
track(ANALYTICS_EVENTS.ONBOARDING_STEP_COMPLETED, {
step_id: currentStep,
step_index: currentIndex,
@@ -87,6 +96,7 @@ export function OnboardingFlow() {
duration_seconds: Math.round(
(Date.now() - stepEnteredAtRef.current) / 1000,
),
+ ...context,
});
};
@@ -101,8 +111,8 @@ export function OnboardingFlow() {
stepEnteredAtRef.current = Date.now();
};
- const handleNext = () => {
- trackStepCompleted();
+ const handleNext = (context?: StepContext) => {
+ trackStepCompleted(context);
trackStepViewed(currentIndex + 1);
next();
};
@@ -112,15 +122,17 @@ export function OnboardingFlow() {
back();
};
- useHotkeys("right", handleNext, { enableOnFormTags: false }, [handleNext]);
+ useHotkeys("right", () => handleNext(), { enableOnFormTags: false }, [
+ handleNext,
+ ]);
useHotkeys("left", handleBack, { enableOnFormTags: false }, [handleBack]);
- const handleComplete = (cliSkipped: boolean) => {
- if (cliSkipped) {
+ const handleComplete = (repoSkipped: boolean) => {
+ if (repoSkipped) {
track(ANALYTICS_EVENTS.ONBOARDING_STEP_SKIPPED, {
step_id: currentStep,
step_index: currentIndex,
- reason: "tools_not_installed",
+ reason: "no_repo_selected",
});
} else {
trackStepCompleted();
@@ -130,7 +142,7 @@ export function OnboardingFlow() {
(Date.now() - flowStartedAtRef.current) / 1000,
),
github_connected: githubUserIntegrations.length > 0,
- cli_skipped: cliSkipped,
+ repo_skipped: repoSkipped,
});
completeOnboarding();
navigateToTaskInput();
@@ -235,9 +247,9 @@ export function OnboardingFlow() {
)}
- {currentStep === "github" && (
+ {currentStep === "connect-github" && (
-
+
)}
@@ -268,7 +273,29 @@ export function OnboardingFlow() {
transition={{ duration: 0.3 }}
className="min-h-0 w-full flex-1"
>
-
+
+
+ )}
+
+ {currentStep === "select-repo" && (
+
+
)}
diff --git a/apps/code/src/renderer/features/onboarding/components/OptionalBadge.tsx b/apps/code/src/renderer/features/onboarding/components/OptionalBadge.tsx
new file mode 100644
index 000000000..1d41c42f1
--- /dev/null
+++ b/apps/code/src/renderer/features/onboarding/components/OptionalBadge.tsx
@@ -0,0 +1,7 @@
+export function OptionalBadge() {
+ return (
+
+ Optional
+
+ );
+}
diff --git a/apps/code/src/renderer/features/onboarding/components/SelectRepoStep.tsx b/apps/code/src/renderer/features/onboarding/components/SelectRepoStep.tsx
new file mode 100644
index 000000000..cd8d8c766
--- /dev/null
+++ b/apps/code/src/renderer/features/onboarding/components/SelectRepoStep.tsx
@@ -0,0 +1,237 @@
+import { FolderPicker } from "@features/folder-picker/components/FolderPicker";
+import { useUserRepositoryIntegration } from "@hooks/useIntegrations";
+import {
+ ArrowLeft,
+ ArrowRight,
+ CheckCircle,
+ CircleNotch,
+ FolderOpen,
+ Lightbulb,
+} from "@phosphor-icons/react";
+import { cn } from "@posthog/quill";
+import { Box, Button, Flex, Text } from "@radix-ui/themes";
+import builderHog from "@renderer/assets/images/hedgehogs/builder-hog-03.png";
+import { AnimatePresence, motion } from "framer-motion";
+import { useMemo } from "react";
+import type { DetectedRepo } from "../hooks/useOnboardingFlow";
+import { OnboardingHogTip } from "./OnboardingHogTip";
+import { OptionalBadge } from "./OptionalBadge";
+import { PANEL_SHADOW } from "./onboardingStyles";
+import { StepActions } from "./StepActions";
+
+interface SelectRepoStepProps {
+ onComplete: (skipped: boolean) => void;
+ onBack: () => void;
+ selectedDirectory: string;
+ detectedRepo: DetectedRepo | null;
+ isDetectingRepo: boolean;
+ onDirectoryChange: (path: string) => void;
+}
+
+export function SelectRepoStep({
+ onComplete,
+ onBack,
+ selectedDirectory,
+ detectedRepo,
+ isDetectingRepo,
+ onDirectoryChange,
+}: SelectRepoStepProps) {
+ const { repositories } = useUserRepositoryIntegration();
+
+ const repoMatchesGitHub = useMemo(() => {
+ if (!detectedRepo || repositories.length === 0) return false;
+ return repositories.some(
+ (r) => r.toLowerCase() === detectedRepo.fullName.toLowerCase(),
+ );
+ }, [detectedRepo, repositories]);
+
+ return (
+
+
+
+
+
+
+
+
+
+ Pick a repo to get started
+
+
+
+
+ We'll scan it and suggest some first things to work on. You
+ can also skip this and start from a blank task.
+
+
+
+
+
+
+
+
+
+
+
+ Choose your repository
+
+
+
+ Select a single repository folder, not a parent folder
+ that contains multiple repos.
+
+
+
+
+ {isDetectingRepo && (
+
+
+
+
+ Detecting repository...
+
+
+
+ )}
+ {!isDetectingRepo &&
+ selectedDirectory &&
+ detectedRepo && (
+
+
+
+
+ {repoMatchesGitHub
+ ? `Linked to ${detectedRepo.fullName} on GitHub`
+ : `Detected ${detectedRepo.fullName}`}
+
+
+
+ )}
+ {!isDetectingRepo &&
+ selectedDirectory &&
+ !detectedRepo && (
+
+
+ No git remote detected. You can still continue.
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ Once you pick a repo we'll look for things like stale
+ feature flags, missing tracking, and other low-effort wins
+ to start from.
+
+
+
+
+
+
+
+
+
+
+
+ {selectedDirectory ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/apps/code/src/renderer/features/onboarding/components/onboardingStyles.ts b/apps/code/src/renderer/features/onboarding/components/onboardingStyles.ts
new file mode 100644
index 000000000..1bd2b9479
--- /dev/null
+++ b/apps/code/src/renderer/features/onboarding/components/onboardingStyles.ts
@@ -0,0 +1,2 @@
+export const PANEL_SHADOW =
+ "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)";
diff --git a/apps/code/src/renderer/features/onboarding/types.ts b/apps/code/src/renderer/features/onboarding/types.ts
index ef03bd3fc..66a118598 100644
--- a/apps/code/src/renderer/features/onboarding/types.ts
+++ b/apps/code/src/renderer/features/onboarding/types.ts
@@ -2,13 +2,15 @@ export type OnboardingStep =
| "welcome"
| "project-select"
| "invite-code"
- | "github"
- | "install-cli";
+ | "connect-github"
+ | "install-cli"
+ | "select-repo";
export const ONBOARDING_STEPS: OnboardingStep[] = [
"welcome",
"project-select",
"invite-code",
- "github",
+ "connect-github",
"install-cli",
+ "select-repo",
];
diff --git a/apps/code/src/shared/types/analytics.ts b/apps/code/src/shared/types/analytics.ts
index 58d12542e..0a4da5c97 100644
--- a/apps/code/src/shared/types/analytics.ts
+++ b/apps/code/src/shared/types/analytics.ts
@@ -324,10 +324,11 @@ export type OnboardingStepId =
| "welcome"
| "project-select"
| "invite-code"
- | "github"
- | "install-cli";
+ | "connect-github"
+ | "install-cli"
+ | "select-repo";
-type OnboardingSkipReason = "tools_not_installed" | "dev_skip";
+type OnboardingSkipReason = "no_repo_selected" | "dev_skip";
export interface OnboardingStepViewedProperties {
step_id: OnboardingStepId;
@@ -340,6 +341,10 @@ export interface OnboardingStepCompletedProperties {
step_index: number;
total_steps: number;
duration_seconds: number;
+ github_connected?: boolean;
+ git_installed?: boolean;
+ gh_installed?: boolean;
+ gh_authenticated?: boolean;
}
export interface OnboardingStepSkippedProperties {
@@ -376,7 +381,22 @@ export interface OnboardingCliCheckCompletedProperties {
export interface OnboardingCompletedProperties {
duration_seconds: number;
github_connected: boolean;
- cli_skipped: boolean;
+ repo_skipped: boolean;
+}
+
+export type OnboardingGithubConnectFlow =
+ | "team_existing"
+ | "team_alternative"
+ | "user_new";
+
+export interface OnboardingGithubConnectStartedProperties {
+ flow_type: OnboardingGithubConnectFlow;
+ is_retry: boolean;
+}
+
+export interface OnboardingGithubConnectFailedProperties {
+ reason: "timeout" | "error";
+ error_type?: string;
}
export interface OnboardingAbandonedProperties {
@@ -681,6 +701,8 @@ export const ANALYTICS_EVENTS = {
ONBOARDING_PROJECT_SELECTED: "Onboarding project selected",
ONBOARDING_INVITE_CODE_SUBMITTED: "Onboarding invite code submitted",
ONBOARDING_FOLDER_SELECTED: "Onboarding folder selected",
+ ONBOARDING_GITHUB_CONNECT_STARTED: "Onboarding github connect started",
+ ONBOARDING_GITHUB_CONNECT_FAILED: "Onboarding github connect failed",
ONBOARDING_GITHUB_CONNECTED: "Onboarding github connected",
ONBOARDING_CLI_CHECK_COMPLETED: "Onboarding cli check completed",
ONBOARDING_COMPLETED: "Onboarding completed",
@@ -797,6 +819,8 @@ export type EventPropertyMap = {
[ANALYTICS_EVENTS.ONBOARDING_PROJECT_SELECTED]: OnboardingProjectSelectedProperties;
[ANALYTICS_EVENTS.ONBOARDING_INVITE_CODE_SUBMITTED]: OnboardingInviteCodeSubmittedProperties;
[ANALYTICS_EVENTS.ONBOARDING_FOLDER_SELECTED]: OnboardingFolderSelectedProperties;
+ [ANALYTICS_EVENTS.ONBOARDING_GITHUB_CONNECT_STARTED]: OnboardingGithubConnectStartedProperties;
+ [ANALYTICS_EVENTS.ONBOARDING_GITHUB_CONNECT_FAILED]: OnboardingGithubConnectFailedProperties;
[ANALYTICS_EVENTS.ONBOARDING_GITHUB_CONNECTED]: never;
[ANALYTICS_EVENTS.ONBOARDING_CLI_CHECK_COMPLETED]: OnboardingCliCheckCompletedProperties;
[ANALYTICS_EVENTS.ONBOARDING_COMPLETED]: OnboardingCompletedProperties;