From 02fcced85c10065ccddec0f856cd8181296d7f09 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 27 May 2026 10:30:45 +0100 Subject: [PATCH 1/7] feat(onboarding): split git connect and repo select into separate steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits the combined "pick repo + connect GitHub" step into two pages: - `connect-git` (optional) — merges the existing CLI install check page with the GitHub OAuth panel. Explains what git integration unlocks (cloud sandboxes, branch pushes, in-app PR review). - `select-repo` (final) — folder picker only, with an explicit "Skip & get started" path. When skipped, `useSetupDiscovery` already no-ops on empty `selectedDirectory`, so enricher-based first-task suggestions naturally skip too. Also retires the `tools_not_installed` skip reason in favor of `no_repo_selected`, and renames `cli_skipped` → `repo_skipped` on the ONBOARDING_COMPLETED event to match the new final step. Generated-By: PostHog Code Task-Id: d15be8ed-c5d9-44cc-8cc7-de90e1e84f2e --- .../onboarding/components/CliInstallStep.tsx | 372 -------- .../onboarding/components/ConnectGitStep.tsx | 899 ++++++++++++++++++ .../components/GitIntegrationStep.tsx | 755 --------------- .../onboarding/components/OnboardingFlow.tsx | 38 +- .../onboarding/components/SelectRepoStep.tsx | 234 +++++ .../src/renderer/features/onboarding/types.ts | 8 +- apps/code/src/shared/types/analytics.ts | 8 +- 7 files changed, 1160 insertions(+), 1154 deletions(-) delete mode 100644 apps/code/src/renderer/features/onboarding/components/CliInstallStep.tsx create mode 100644 apps/code/src/renderer/features/onboarding/components/ConnectGitStep.tsx delete mode 100644 apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx create mode 100644 apps/code/src/renderer/features/onboarding/components/SelectRepoStep.tsx 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 718876d310..0000000000 --- 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/ConnectGitStep.tsx b/apps/code/src/renderer/features/onboarding/components/ConnectGitStep.tsx new file mode 100644 index 0000000000..c2df4a323a --- /dev/null +++ b/apps/code/src/renderer/features/onboarding/components/ConnectGitStep.tsx @@ -0,0 +1,899 @@ +import { Tooltip } from "@components/ui/Tooltip"; +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 { + ArrowLeft, + ArrowRight, + ArrowSquareOut, + ArrowsClockwise, + Check, + CheckCircle, + CircleNotch, + Cloud, + Copy, + GearSix, + GitBranch, + GithubLogo, + GitPullRequest, + Plus, + Warning, +} from "@phosphor-icons/react"; +import { + AlertDialog, + Box, + Button, + DropdownMenu, + Flex, + IconButton, + Skeleton, + Spinner, + 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 { useMutation, 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, useMemo, useRef, useState } from "react"; +import { toast } from "sonner"; +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 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 ? : } + + + + ); +} + +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."; +} + +interface ConnectGitStepProps { + onNext: () => void; + onBack: () => void; +} + +export function ConnectGitStep({ onNext, onBack }: ConnectGitStepProps) { + 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 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 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 Git + + + Optional, but it 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. + + + + + + + + + + + + + + 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} + + + + + + + + + + + + Git + + + {isLoadingGit && ( + + )} + {!isLoadingGit && gitInstalled && ( + + + + Installed + {gitStatus?.version + ? ` (${gitStatus.version})` + : ""} + + + )} + + {!isLoadingGit && !gitInstalled && ( + + + Install with Homebrew or Xcode Command Line Tools: + + + + + + + + + + + )} + + + + + + + + + + + + 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: + + + + + + + )} + + + + + + + + + + + + + + + { + 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 3fd3b060c1..0000000000 --- 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/OnboardingFlow.tsx b/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx index 985f5826f5..0e792ba835 100644 --- a/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx +++ b/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx @@ -14,10 +14,10 @@ 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 { ConnectGitStep } from "./ConnectGitStep"; import { InviteCodeStep } from "./InviteCodeStep"; import { ProjectSelectStep } from "./ProjectSelectStep"; +import { SelectRepoStep } from "./SelectRepoStep"; import { StepIndicator } from "./StepIndicator"; import { WelcomeScreen } from "./WelcomeScreen"; @@ -115,12 +115,12 @@ export function OnboardingFlow() { 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 +130,7 @@ export function OnboardingFlow() { (Date.now() - flowStartedAtRef.current) / 1000, ), github_connected: githubUserIntegrations.length > 0, - cli_skipped: cliSkipped, + repo_skipped: repoSkipped, }); completeOnboarding(); navigateToTaskInput(); @@ -235,9 +235,9 @@ export function OnboardingFlow() { )} - {currentStep === "github" && ( + {currentStep === "connect-git" && ( - + )} - {currentStep === "install-cli" && ( + {currentStep === "select-repo" && ( - + )} 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 0000000000..5de95d2d69 --- /dev/null +++ b/apps/code/src/renderer/features/onboarding/components/SelectRepoStep.tsx @@ -0,0 +1,234 @@ +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 { StepActions } from "./StepActions"; + +const PANEL_SHADOW = "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)"; + +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/types.ts b/apps/code/src/renderer/features/onboarding/types.ts index ef03bd3fc0..08e392ad5e 100644 --- a/apps/code/src/renderer/features/onboarding/types.ts +++ b/apps/code/src/renderer/features/onboarding/types.ts @@ -2,13 +2,13 @@ export type OnboardingStep = | "welcome" | "project-select" | "invite-code" - | "github" - | "install-cli"; + | "connect-git" + | "select-repo"; export const ONBOARDING_STEPS: OnboardingStep[] = [ "welcome", "project-select", "invite-code", - "github", - "install-cli", + "connect-git", + "select-repo", ]; diff --git a/apps/code/src/shared/types/analytics.ts b/apps/code/src/shared/types/analytics.ts index 58d12542e6..d844924360 100644 --- a/apps/code/src/shared/types/analytics.ts +++ b/apps/code/src/shared/types/analytics.ts @@ -324,10 +324,10 @@ export type OnboardingStepId = | "welcome" | "project-select" | "invite-code" - | "github" - | "install-cli"; + | "connect-git" + | "select-repo"; -type OnboardingSkipReason = "tools_not_installed" | "dev_skip"; +type OnboardingSkipReason = "no_repo_selected" | "dev_skip"; export interface OnboardingStepViewedProperties { step_id: OnboardingStepId; @@ -376,7 +376,7 @@ export interface OnboardingCliCheckCompletedProperties { export interface OnboardingCompletedProperties { duration_seconds: number; github_connected: boolean; - cli_skipped: boolean; + repo_skipped: boolean; } export interface OnboardingAbandonedProperties { From 252e7d244377dceeed967c6730422a00e4f7ff61 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 27 May 2026 10:36:41 +0100 Subject: [PATCH 2/7] refactor(onboarding): split ConnectGitStep into smaller components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ConnectGitStep was ~900 lines covering three independent concerns. Split into: - `GitHubConnectPanel` — self-contained GitHub OAuth UI (status header, alternative-project copy, connect buttons, integration rows, disconnect dialog). - `CliCheckPanel` + `InstalledBadge` — generic shell for the Git / GitHub CLI install panels, deduping the near-identical Box + header + spinner +status-badge scaffold. - `ConnectGitStep` — now ~350 lines, orchestrates the header, benefits list, the three panels, and step actions. No behavior change. Generated-By: PostHog Code Task-Id: d15be8ed-c5d9-44cc-8cc7-de90e1e84f2e --- .../onboarding/components/CliCheckPanel.tsx | 59 ++ .../onboarding/components/ConnectGitStep.tsx | 808 +++--------------- .../components/GitHubConnectPanel.tsx | 498 +++++++++++ 3 files changed, 685 insertions(+), 680 deletions(-) create mode 100644 apps/code/src/renderer/features/onboarding/components/CliCheckPanel.tsx create mode 100644 apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx 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 0000000000..f901cdddca --- /dev/null +++ b/apps/code/src/renderer/features/onboarding/components/CliCheckPanel.tsx @@ -0,0 +1,59 @@ +import { CheckCircle, CircleNotch } from "@phosphor-icons/react"; +import { Box, Flex, Text } from "@radix-ui/themes"; +import type { ReactNode } from "react"; + +const PANEL_SHADOW = "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)"; + +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/ConnectGitStep.tsx b/apps/code/src/renderer/features/onboarding/components/ConnectGitStep.tsx index c2df4a323a..d266974e28 100644 --- a/apps/code/src/renderer/features/onboarding/components/ConnectGitStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/ConnectGitStep.tsx @@ -1,16 +1,4 @@ import { Tooltip } from "@components/ui/Tooltip"; -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 { ArrowLeft, ArrowRight, @@ -18,42 +6,27 @@ import { ArrowsClockwise, Check, CheckCircle, - CircleNotch, Cloud, Copy, - GearSix, GitBranch, GithubLogo, GitPullRequest, - Plus, Warning, } from "@phosphor-icons/react"; -import { - AlertDialog, - Box, - Button, - DropdownMenu, - Flex, - IconButton, - Skeleton, - Spinner, - Text, -} from "@radix-ui/themes"; +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 } from "@shared/types/analytics"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +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, useMemo, useRef, useState } from "react"; -import { toast } from "sonner"; -import { useProjectsWithIntegrations } from "../hooks/useProjectsWithIntegrations"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { CliCheckPanel, InstalledBadge } from "./CliCheckPanel"; +import { GitHubConnectPanel } from "./GitHubConnectPanel"; 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 CommandLine({ command }: { command: string }) { const [copied, setCopied] = useState(false); @@ -93,21 +66,6 @@ function CommandLine({ command }: { command: string }) { ); } -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."; -} - interface ConnectGitStepProps { onNext: () => void; onBack: () => void; @@ -153,98 +111,6 @@ export function ConnectGitStep({ onNext, onBack }: ConnectGitStepProps) { setIsCheckingGh(false); }, [queryClient, trpc]); - 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 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"} - - - ) - ) : ( - - 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} - - + - } + title="Git" + isLoading={isLoadingGit} + statusBadge={ + gitInstalled ? ( + + ) : null + } > - - - - - - Git - + {!isLoadingGit && !gitInstalled && ( + + + Install with Homebrew or Xcode Command Line Tools: + + + + - {isLoadingGit && ( - - )} - {!isLoadingGit && gitInstalled && ( - - - - Installed - {gitStatus?.version - ? ` (${gitStatus.version})` - : ""} - - - )} - - {!isLoadingGit && !gitInstalled && ( - - - Install with Homebrew or Xcode Command Line Tools: - - - - - - - - - + + + - )} - - + + )} + - - - - - - - GitHub CLI - - - {isLoadingGh && ( - } + title="GitHub CLI" + isLoading={isLoadingGh} + statusBadge={ + ghInstalled && ghAuthenticated ? ( + + ) : ghInstalled ? ( + + - )} - {!isLoadingGh && ghInstalled && ghAuthenticated && ( - - - - {ghStatus?.username - ? `Logged in as ${ghStatus.username}` - : "Authenticated"} - - - )} - {!isLoadingGh && ghInstalled && !ghAuthenticated && ( - - - - Not logged in - - - )} - - {!isLoadingGh && !ghInstalled && ( - - - Install with Homebrew: + + Not logged in - - - - - - )} - {!isLoadingGh && ghInstalled && !ghAuthenticated && ( - - - Run this in your terminal to log in: - - - - - + ) : null + } + > + {!isLoadingGh && !ghInstalled && ( + + + Install with Homebrew: + + + + + - )} - - + + )} + {!isLoadingGh && ghInstalled && !ghAuthenticated && ( + + + Run this in your terminal to log in: + + + + + + + )} + @@ -846,53 +341,6 @@ export function ConnectGitStep({ onNext, onBack }: ConnectGitStepProps) { - - { - 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/GitHubConnectPanel.tsx b/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx new file mode 100644 index 0000000000..a38b5da9bf --- /dev/null +++ b/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx @@ -0,0 +1,498 @@ +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 } from "@shared/types/analytics"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { track } from "@utils/analytics"; +import { useMemo, useState } from "react"; +import { toast } from "sonner"; +import { useProjectsWithIntegrations } from "../hooks/useProjectsWithIntegrations"; + +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 "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 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"} + + + ) + ) : ( + + 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} + + + + { + 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. + + + + + + + + + + + ); +} From 2f442e5067fe5e6638682587566efe34b0dc7aee Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 27 May 2026 11:26:11 +0100 Subject: [PATCH 3/7] feat(onboarding): instrument connect-git step with tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds tracking for the new connect-git step, following the analytics conventions in docs/conventions.md (Object verbed naming, generic-event- with-discriminator-property over bespoke events). - `Onboarding github connect started` — fires on any GitHub connect button click, with `flow_type` (team_existing / team_alternative / user_new) and `is_retry`. Captures the click→success funnel that `Onboarding github connected` (success) alone can't measure. - `Onboarding github connect failed` — fires on OAuth error / timeout, deduped per failure fingerprint so it doesn't refire on re-renders. Properties: `reason` (timeout / error), `error_type` (from the GithubUserConnectError code). - `Onboarding step completed` extended with optional context fields (`github_connected`, `git_installed`, `gh_installed`, `gh_authenticated`) populated when leaving the connect-git step, so the snapshot at the decision point rides on the existing generic step event rather than on a bespoke `Onboarding git setup completed` event. `onNext` now optionally accepts a step-completion context, threaded through to `trackStepCompleted`. Other steps continue to call `onNext()` with no args. Generated-By: PostHog Code Task-Id: d15be8ed-c5d9-44cc-8cc7-de90e1e84f2e --- .../onboarding/components/ConnectGitStep.tsx | 25 ++++++++-- .../components/GitHubConnectPanel.tsx | 47 ++++++++++++++++--- .../onboarding/components/OnboardingFlow.tsx | 21 +++++++-- apps/code/src/shared/types/analytics.ts | 23 +++++++++ 4 files changed, 102 insertions(+), 14 deletions(-) diff --git a/apps/code/src/renderer/features/onboarding/components/ConnectGitStep.tsx b/apps/code/src/renderer/features/onboarding/components/ConnectGitStep.tsx index d266974e28..598e560df3 100644 --- a/apps/code/src/renderer/features/onboarding/components/ConnectGitStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/ConnectGitStep.tsx @@ -1,4 +1,5 @@ import { Tooltip } from "@components/ui/Tooltip"; +import { useUserGithubIntegrations } from "@hooks/useIntegrations"; import { ArrowLeft, ArrowRight, @@ -16,7 +17,10 @@ import { 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 } from "@shared/types/analytics"; +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"; @@ -66,8 +70,13 @@ function CommandLine({ command }: { command: string }) { ); } +type StepContext = Pick< + OnboardingStepCompletedProperties, + "github_connected" | "git_installed" | "gh_installed" | "gh_authenticated" +>; + interface ConnectGitStepProps { - onNext: () => void; + onNext: (context?: StepContext) => void; onBack: () => void; } @@ -111,6 +120,16 @@ export function ConnectGitStep({ onNext, onBack }: ConnectGitStepProps) { setIsCheckingGh(false); }, [queryClient, trpc]); + const { data: githubUserIntegrations = [] } = useUserGithubIntegrations(); + const handleContinue = () => { + onNext({ + github_connected: githubUserIntegrations.length > 0, + git_installed: gitInstalled, + gh_installed: ghInstalled, + gh_authenticated: ghAuthenticated, + }); + }; + return ( Back - diff --git a/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx b/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx index a38b5da9bf..0de64db18e 100644 --- a/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx +++ b/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx @@ -29,10 +29,13 @@ import { Text, } from "@radix-ui/themes"; import { trpcClient } from "@renderer/trpc/client"; -import { ANALYTICS_EVENTS } from "@shared/types/analytics"; +import { + ANALYTICS_EVENTS, + type OnboardingGithubConnectFlow, +} from "@shared/types/analytics"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { track } from "@utils/analytics"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { useProjectsWithIntegrations } from "../hooks/useProjectsWithIntegrations"; @@ -86,6 +89,33 @@ export function GitHubConnectPanel() { 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, @@ -304,6 +334,10 @@ export function GitHubConnectPanel() { !isReconnecting } onClick={async () => { + track( + ANALYTICS_EVENTS.ONBOARDING_GITHUB_CONNECT_STARTED, + { flow_type: "user_new", is_retry: true }, + ); setReconnectingInstallationId(installationId); try { await disconnectMutation.mutateAsync({ @@ -376,7 +410,7 @@ export function GitHubConnectPanel() { size="1" variant="ghost" color="gray" - onClick={() => void handleConnectGitHub()} + onClick={() => initiateConnect("user_new")} loading={isConnecting} > @@ -389,7 +423,7 @@ export function GitHubConnectPanel() { + + + + + ); +} diff --git a/apps/code/src/renderer/features/onboarding/components/ConnectGitStep.tsx b/apps/code/src/renderer/features/onboarding/components/InstallCliStep.tsx similarity index 83% rename from apps/code/src/renderer/features/onboarding/components/ConnectGitStep.tsx rename to apps/code/src/renderer/features/onboarding/components/InstallCliStep.tsx index 598e560df3..a645e8b636 100644 --- a/apps/code/src/renderer/features/onboarding/components/ConnectGitStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/InstallCliStep.tsx @@ -1,17 +1,13 @@ import { Tooltip } from "@components/ui/Tooltip"; -import { useUserGithubIntegrations } from "@hooks/useIntegrations"; import { ArrowLeft, ArrowRight, ArrowSquareOut, ArrowsClockwise, Check, - CheckCircle, - Cloud, Copy, GitBranch, GithubLogo, - GitPullRequest, Warning, } from "@phosphor-icons/react"; import { Button, Flex, IconButton, Text } from "@radix-ui/themes"; @@ -27,7 +23,6 @@ import { EXTERNAL_LINKS } from "@utils/links"; import { motion } from "framer-motion"; import { useCallback, useEffect, useRef, useState } from "react"; import { CliCheckPanel, InstalledBadge } from "./CliCheckPanel"; -import { GitHubConnectPanel } from "./GitHubConnectPanel"; import { OnboardingHogTip } from "./OnboardingHogTip"; import { StepActions } from "./StepActions"; @@ -72,15 +67,15 @@ function CommandLine({ command }: { command: string }) { type StepContext = Pick< OnboardingStepCompletedProperties, - "github_connected" | "git_installed" | "gh_installed" | "gh_authenticated" + "git_installed" | "gh_installed" | "gh_authenticated" >; -interface ConnectGitStepProps { +interface InstallCliStepProps { onNext: (context?: StepContext) => void; onBack: () => void; } -export function ConnectGitStep({ onNext, onBack }: ConnectGitStepProps) { +export function InstallCliStep({ onNext, onBack }: InstallCliStepProps) { const trpc = useTRPC(); const queryClient = useQueryClient(); @@ -120,10 +115,8 @@ export function ConnectGitStep({ onNext, onBack }: ConnectGitStepProps) { setIsCheckingGh(false); }, [queryClient, trpc]); - const { data: githubUserIntegrations = [] } = useUserGithubIntegrations(); const handleContinue = () => { onNext({ - github_connected: githubUserIntegrations.length > 0, git_installed: gitInstalled, gh_installed: ghInstalled, gh_authenticated: ghAuthenticated, @@ -151,56 +144,19 @@ export function ConnectGitStep({ onNext, onBack }: ConnectGitStepProps) { > - Connect Git + Install CLI tools - Optional, but it unlocks the parts of PostHog Code that - leave your machine. + Optional. Agents use these to manage branches and open pull + requests on your behalf. - - - - - 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. - - - - - - - - - - } @@ -256,7 +212,7 @@ export function ConnectGitStep({ onNext, onBack }: ConnectGitStepProps) { } @@ -344,7 +300,7 @@ export function ConnectGitStep({ onNext, onBack }: ConnectGitStepProps) { diff --git a/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx b/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx index 195dda0343..33949aa4d2 100644 --- a/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx +++ b/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx @@ -17,7 +17,8 @@ import { useEffect, useRef } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { useOnboardingFlow } from "../hooks/useOnboardingFlow"; -import { ConnectGitStep } from "./ConnectGitStep"; +import { ConnectGitHubStep } from "./ConnectGitHubStep"; +import { InstallCliStep } from "./InstallCliStep"; import { InviteCodeStep } from "./InviteCodeStep"; import { ProjectSelectStep } from "./ProjectSelectStep"; import { SelectRepoStep } from "./SelectRepoStep"; @@ -246,9 +247,9 @@ export function OnboardingFlow() { )} - {currentStep === "connect-git" && ( + {currentStep === "connect-github" && ( - + + + )} + + {currentStep === "install-cli" && ( + + )} diff --git a/apps/code/src/renderer/features/onboarding/types.ts b/apps/code/src/renderer/features/onboarding/types.ts index 08e392ad5e..66a118598a 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" - | "connect-git" + | "connect-github" + | "install-cli" | "select-repo"; export const ONBOARDING_STEPS: OnboardingStep[] = [ "welcome", "project-select", "invite-code", - "connect-git", + "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 2bd5b62c3c..0a4da5c970 100644 --- a/apps/code/src/shared/types/analytics.ts +++ b/apps/code/src/shared/types/analytics.ts @@ -324,7 +324,8 @@ export type OnboardingStepId = | "welcome" | "project-select" | "invite-code" - | "connect-git" + | "connect-github" + | "install-cli" | "select-repo"; type OnboardingSkipReason = "no_repo_selected" | "dev_skip"; From c23c12c4ae1393ad9c464fce502dc5f8eb3e3698 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 27 May 2026 12:53:26 +0100 Subject: [PATCH 5/7] refactor(onboarding): show Optional badge next to optional step titles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracts the existing inline "Optional" pill from GitHubConnectPanel into a shared OptionalBadge component and renders it next to the connect-github and install-cli step titles. The body copy no longer leads with "Optional," — it now reads naturally as a description of what the step unlocks. Generated-By: PostHog Code Task-Id: d15be8ed-c5d9-44cc-8cc7-de90e1e84f2e --- .../onboarding/components/ConnectGitHubStep.tsx | 13 ++++++++----- .../onboarding/components/GitHubConnectPanel.tsx | 5 ++--- .../onboarding/components/InstallCliStep.tsx | 14 +++++++++----- .../onboarding/components/OptionalBadge.tsx | 7 +++++++ 4 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 apps/code/src/renderer/features/onboarding/components/OptionalBadge.tsx diff --git a/apps/code/src/renderer/features/onboarding/components/ConnectGitHubStep.tsx b/apps/code/src/renderer/features/onboarding/components/ConnectGitHubStep.tsx index 2f1362c074..43bbee7a5a 100644 --- a/apps/code/src/renderer/features/onboarding/components/ConnectGitHubStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/ConnectGitHubStep.tsx @@ -12,6 +12,7 @@ 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; @@ -47,12 +48,14 @@ export function ConnectGitHubStep({ onNext, onBack }: ConnectGitHubStepProps) { transition={{ duration: 0.3 }} > - - Connect GitHub - + + + Connect GitHub + + + - Optional, but it unlocks the parts of PostHog Code that - leave your machine. + Unlocks the parts of PostHog Code that leave your machine. diff --git a/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx b/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx index 0de64db18e..1ad2972da4 100644 --- a/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx +++ b/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx @@ -38,6 +38,7 @@ 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"; const PANEL_SHADOW = "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)"; @@ -215,9 +216,7 @@ export function GitHubConnectPanel() { ) ) : ( - - Optional - + )} {!hasGitIntegration && diff --git a/apps/code/src/renderer/features/onboarding/components/InstallCliStep.tsx b/apps/code/src/renderer/features/onboarding/components/InstallCliStep.tsx index a645e8b636..d988d27015 100644 --- a/apps/code/src/renderer/features/onboarding/components/InstallCliStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/InstallCliStep.tsx @@ -24,6 +24,7 @@ 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 }) { @@ -143,12 +144,15 @@ export function InstallCliStep({ onNext, onBack }: InstallCliStepProps) { transition={{ duration: 0.3 }} > - - Install CLI tools - + + + Install CLI tools + + + - Optional. Agents use these to manage branches and open pull - requests on your behalf. + Agents use these to manage branches and open pull requests + on your behalf. 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 0000000000..1d41c42f11 --- /dev/null +++ b/apps/code/src/renderer/features/onboarding/components/OptionalBadge.tsx @@ -0,0 +1,7 @@ +export function OptionalBadge() { + return ( + + Optional + + ); +} From 498e8dff0f065315e2ba209639f4c9e0dd078404 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 27 May 2026 14:23:15 +0100 Subject: [PATCH 6/7] refactor(onboarding): dedupe PANEL_SHADOW into shared module Three onboarding components defined an identical PANEL_SHADOW constant. Extract it to onboardingStyles.ts so future theme changes are a one-line edit. Generated-By: PostHog Code Task-Id: d15be8ed-c5d9-44cc-8cc7-de90e1e84f2e --- .../renderer/features/onboarding/components/CliCheckPanel.tsx | 3 +-- .../features/onboarding/components/GitHubConnectPanel.tsx | 3 +-- .../renderer/features/onboarding/components/SelectRepoStep.tsx | 3 +-- .../features/onboarding/components/onboardingStyles.ts | 2 ++ 4 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 apps/code/src/renderer/features/onboarding/components/onboardingStyles.ts diff --git a/apps/code/src/renderer/features/onboarding/components/CliCheckPanel.tsx b/apps/code/src/renderer/features/onboarding/components/CliCheckPanel.tsx index f901cdddca..9da6107a56 100644 --- a/apps/code/src/renderer/features/onboarding/components/CliCheckPanel.tsx +++ b/apps/code/src/renderer/features/onboarding/components/CliCheckPanel.tsx @@ -1,8 +1,7 @@ import { CheckCircle, CircleNotch } from "@phosphor-icons/react"; import { Box, Flex, Text } from "@radix-ui/themes"; import type { ReactNode } from "react"; - -const PANEL_SHADOW = "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)"; +import { PANEL_SHADOW } from "./onboardingStyles"; interface CliCheckPanelProps { icon: ReactNode; diff --git a/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx b/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx index 1ad2972da4..08119f7a62 100644 --- a/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx +++ b/apps/code/src/renderer/features/onboarding/components/GitHubConnectPanel.tsx @@ -39,8 +39,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { useProjectsWithIntegrations } from "../hooks/useProjectsWithIntegrations"; import { OptionalBadge } from "./OptionalBadge"; - -const PANEL_SHADOW = "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)"; +import { PANEL_SHADOW } from "./onboardingStyles"; function getPanelMessage(opts: { hasConnectError: boolean; diff --git a/apps/code/src/renderer/features/onboarding/components/SelectRepoStep.tsx b/apps/code/src/renderer/features/onboarding/components/SelectRepoStep.tsx index 5de95d2d69..ec3193a081 100644 --- a/apps/code/src/renderer/features/onboarding/components/SelectRepoStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/SelectRepoStep.tsx @@ -15,10 +15,9 @@ import { AnimatePresence, motion } from "framer-motion"; import { useMemo } from "react"; import type { DetectedRepo } from "../hooks/useOnboardingFlow"; import { OnboardingHogTip } from "./OnboardingHogTip"; +import { PANEL_SHADOW } from "./onboardingStyles"; 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)"; - interface SelectRepoStepProps { onComplete: (skipped: boolean) => void; onBack: () => void; 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 0000000000..1bd2b94791 --- /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)"; From 60c0db962be457b0599e6077fd310d64634e9e0c Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 27 May 2026 14:36:14 +0100 Subject: [PATCH 7/7] refactor(onboarding): show Optional badge next to Pick a repo title Generated-By: PostHog Code Task-Id: d15be8ed-c5d9-44cc-8cc7-de90e1e84f2e --- .../features/onboarding/components/SelectRepoStep.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/code/src/renderer/features/onboarding/components/SelectRepoStep.tsx b/apps/code/src/renderer/features/onboarding/components/SelectRepoStep.tsx index ec3193a081..cd8d8c766d 100644 --- a/apps/code/src/renderer/features/onboarding/components/SelectRepoStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/SelectRepoStep.tsx @@ -15,6 +15,7 @@ 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"; @@ -64,9 +65,12 @@ export function SelectRepoStep({ transition={{ duration: 0.3 }} > - - Pick a repo to get started - + + + 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.