From e01d637d97c06b5008b61d74d2c016f6e5850ee3 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 6 May 2026 07:53:42 +0100 Subject: [PATCH] feat(code): unify git integration to use git store and always use new git user integration --- .../inbox/components/DataSourceSetup.tsx | 116 +++++------------ .../list/GitHubConnectionBanner.tsx | 10 +- .../hooks/useGithubUserConnect.ts | 19 ++- .../components/GitIntegrationStep.tsx | 7 +- .../sections/GitHubIntegrationSection.tsx | 122 ++++-------------- .../components/sections/GitHubSettings.tsx | 10 +- .../components/CloudGithubMissingNotice.tsx | 6 +- 7 files changed, 91 insertions(+), 199 deletions(-) diff --git a/apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx b/apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx index ae0d944cb..b3022342e 100644 --- a/apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx +++ b/apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx @@ -1,6 +1,10 @@ import { useAuthenticatedClient } from "@features/auth/hooks/authClient"; import { useAuthStateValue } from "@features/auth/hooks/authQueries"; import { GitHubRepoPicker } from "@features/folder-picker/components/GitHubRepoPicker"; +import { + describeGithubConnectError, + useGithubUserConnect, +} from "@features/integrations/hooks/useGithubUserConnect"; import { useGithubRepositories, useRepositoryIntegration, @@ -55,12 +59,8 @@ interface SetupFormProps { onCancel: () => void; } -const POLL_INTERVAL_GITHUB_MS = 3_000; -const POLL_TIMEOUT_GITHUB_MS = 300_000; - function GitHubSetup({ onComplete, onCancel }: SetupFormProps) { const projectId = useAuthStateValue((state) => state.projectId); - const cloudRegion = useAuthStateValue((state) => state.cloudRegion); const client = useAuthenticatedClient(); const { repositories, @@ -80,26 +80,17 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) { } = useGithubRepositories(repoPickerSearchQuery, isRepoPickerOpen); const [repo, setRepo] = useState(null); const [loading, setLoading] = useState(false); - const [connecting, setConnecting] = useState(false); - const pollTimerRef = useRef | null>(null); - const pollTimeoutRef = useRef | null>(null); + const { + error: connectError, + isConnecting: connecting, + isTimedOut: timedOut, + hasError: hasConnectError, + connect: handleConnectGitHub, + } = useGithubUserConnect({ projectId }); const selectedIntegrationId = repo ? getIntegrationIdForRepo(repo) : undefined; - const stopPolling = useCallback(() => { - if (pollTimerRef.current) { - clearInterval(pollTimerRef.current); - pollTimerRef.current = null; - } - if (pollTimeoutRef.current) { - clearTimeout(pollTimeoutRef.current); - pollTimeoutRef.current = null; - } - }, []); - - useEffect(() => stopPolling, [stopPolling]); - useEffect(() => { if (isLoadingRepos || !repo || repositories.includes(repo)) { return; @@ -108,14 +99,6 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) { setRepo(null); }, [isLoadingRepos, repo, repositories]); - // Stop polling once integration appears - useEffect(() => { - if (hasGithubIntegration && connecting) { - stopPolling(); - setConnecting(false); - } - }, [hasGithubIntegration, connecting, stopPolling]); - // Auto-select the first repo once loaded useEffect(() => { if (repo === null && repositories.length > 0) { @@ -123,47 +106,6 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) { } }, [repo, repositories]); - const handleConnectGitHub = useCallback(async () => { - if (!cloudRegion || !projectId) return; - setConnecting(true); - try { - await trpcClient.githubIntegration.startFlow.mutate({ - region: cloudRegion, - projectId, - }); - - pollTimerRef.current = setInterval(async () => { - try { - if (!client) return; - // Trigger a refetch of integrations - const integrations = - await client.getIntegrationsForProject(projectId); - const hasGithub = integrations.some( - (i: { kind: string }) => i.kind === "github", - ); - if (hasGithub) { - stopPolling(); - setConnecting(false); - toast.success("GitHub connected"); - } - } catch { - // Ignore individual poll failures - } - }, POLL_INTERVAL_GITHUB_MS); - - pollTimeoutRef.current = setTimeout(() => { - stopPolling(); - setConnecting(false); - toast.error("Connection timed out. Please try again."); - }, POLL_TIMEOUT_GITHUB_MS); - } catch (error) { - setConnecting(false); - toast.error( - error instanceof Error ? error.message : "Failed to start GitHub flow", - ); - } - }, [cloudRegion, projectId, client, stopPolling]); - const handleSubmit = useCallback(async () => { if (!projectId || !client || !repo || !selectedIntegrationId) return; @@ -216,24 +158,28 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) { setRepoPickerSearchQuery(value); }, []); - const handleLoadMoreRepositories = useCallback(() => { - loadMoreVisibleRepositories(); - }, [loadMoreVisibleRepositories]); - if (!hasGithubIntegration) { + const statusMessage = hasConnectError + ? describeGithubConnectError(connectError) + : timedOut + ? "We didn't hear back from GitHub. If the browser tab was closed, click Try again." + : connecting + ? "Waiting for GitHub… finish authorizing in your browser, then return here." + : "Connect your GitHub account to import issues as signals."; return ( - - Connect your GitHub account to import issues as signals. + + {statusMessage} - @@ -266,7 +216,7 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) { searchQuery={repoPickerSearchQuery} onSearchQueryChange={handleRepoPickerSearchChange} hasMore={visibleRepositoriesHasMore} - onLoadMore={handleLoadMoreRepositories} + onLoadMore={loadMoreVisibleRepositories} placeholder="Select repository..." size="2" /> diff --git a/apps/code/src/renderer/features/inbox/components/list/GitHubConnectionBanner.tsx b/apps/code/src/renderer/features/inbox/components/list/GitHubConnectionBanner.tsx index fc9e68715..449e33dee 100644 --- a/apps/code/src/renderer/features/inbox/components/list/GitHubConnectionBanner.tsx +++ b/apps/code/src/renderer/features/inbox/components/list/GitHubConnectionBanner.tsx @@ -24,9 +24,13 @@ export function GitHubConnectionBanner() { const projectId = useAuthStateValue((s) => s.projectId); const cloudRegion = useAuthStateValue((s) => s.cloudRegion); - const { state, error, connect, reset } = useGithubUserConnect({ projectId }); - const connecting = state === "connecting"; - const hasConnectError = state === "error"; + const { + error, + isConnecting: connecting, + hasError: hasConnectError, + connect, + reset, + } = useGithubUserConnect({ projectId }); const canConnectCloud = projectId != null && cloudRegion != null; if (loginLoading) { diff --git a/apps/code/src/renderer/features/integrations/hooks/useGithubUserConnect.ts b/apps/code/src/renderer/features/integrations/hooks/useGithubUserConnect.ts index 3a51b71d9..ca35e6908 100644 --- a/apps/code/src/renderer/features/integrations/hooks/useGithubUserConnect.ts +++ b/apps/code/src/renderer/features/integrations/hooks/useGithubUserConnect.ts @@ -1,5 +1,4 @@ import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient"; -import { useAuthStateValue } from "@features/auth/hooks/authQueries"; import { useGitHubIntegrationCallback } from "@features/integrations/hooks/useGitHubIntegrationCallback"; import { trpcClient } from "@renderer/trpc/client"; import { IS_DEV } from "@shared/constants/environment"; @@ -64,6 +63,9 @@ interface Options { interface Result { state: GithubUserConnectState; error: GithubUserConnectError | null; + isConnecting: boolean; + isTimedOut: boolean; + hasError: boolean; connect: () => Promise; reset: () => void; } @@ -96,7 +98,6 @@ export async function openUrlInBrowser(url: string): Promise { export function useGithubUserConnect({ projectId }: Options): Result { const client = useOptionalAuthenticatedClient(); - const cloudRegion = useAuthStateValue((s) => s.cloudRegion); const queryClient = useQueryClient(); const [state, setState] = useState("idle"); const [error, setError] = useState(null); @@ -153,7 +154,7 @@ export function useGithubUserConnect({ projectId }: Options): Result { const connect = useCallback(async () => { if (stateRef.current === "connecting") return; - if (!cloudRegion || projectId === null || !client) return; + if (projectId === null || !client) return; stopPolling(); setError(null); setState("connecting"); @@ -184,7 +185,7 @@ export function useGithubUserConnect({ projectId }: Options): Result { code: null, }); } - }, [client, cloudRegion, projectId, invalidate, stopPolling]); + }, [client, projectId, invalidate, stopPolling]); const reset = useCallback(() => { stopPolling(); @@ -192,5 +193,13 @@ export function useGithubUserConnect({ projectId }: Options): Result { setState("idle"); }, [stopPolling]); - return { state, error, connect, reset }; + return { + state, + error, + isConnecting: state === "connecting", + isTimedOut: state === "timed-out", + hasError: state === "error", + connect, + reset, + }; } diff --git a/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx b/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx index ec01fbeb0..e7aef796c 100644 --- a/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx @@ -84,14 +84,13 @@ export function GitIntegrationStep({ }, [manuallySelectedProjectId, currentProjectId, projects]); const { - state: connectState, error: connectError, + isConnecting, + isTimedOut: timedOut, + hasError: hasConnectError, connect: handleConnectGitHub, reset: resetConnect, } = useGithubUserConnect({ projectId: selectedProjectId }); - const isConnecting = connectState === "connecting"; - const timedOut = connectState === "timed-out"; - const hasConnectError = connectState === "error"; const canTakeAction = !isConnecting && !timedOut && !hasConnectError; const defaultPanelMessage = hasConnectError ? describeGithubConnectError(connectError) diff --git a/apps/code/src/renderer/features/settings/components/sections/GitHubIntegrationSection.tsx b/apps/code/src/renderer/features/settings/components/sections/GitHubIntegrationSection.tsx index 93639605c..40298078d 100644 --- a/apps/code/src/renderer/features/settings/components/sections/GitHubIntegrationSection.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/GitHubIntegrationSection.tsx @@ -1,4 +1,8 @@ import { useAuthStateValue } from "@features/auth/hooks/authQueries"; +import { + describeGithubConnectError, + useGithubUserConnect, +} from "@features/integrations/hooks/useGithubUserConnect"; import { useRepositoryIntegration } from "@hooks/useIntegrations"; import { ArrowSquareOutIcon, @@ -7,15 +11,6 @@ import { InfoIcon, } from "@phosphor-icons/react"; import { Box, Button, Flex, Spinner, Text, Tooltip } from "@radix-ui/themes"; -import { useGitHubIntegrationCallback } from "@renderer/features/integrations/hooks/useGitHubIntegrationCallback"; -import { trpcClient } from "@renderer/trpc/client"; -import { IS_DEV } from "@shared/constants/environment"; -import { useQueryClient } from "@tanstack/react-query"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { toast } from "sonner"; - -const POLL_INTERVAL_MS = 3_000; -const POLL_TIMEOUT_MS = 300_000; // 5 minutes export function GitHubIntegrationSection({ hasGithubIntegration, @@ -24,90 +19,13 @@ export function GitHubIntegrationSection({ }) { const { repositories, isLoadingRepos } = useRepositoryIntegration(); const projectId = useAuthStateValue((state) => state.projectId); - const cloudRegion = useAuthStateValue((state) => state.cloudRegion); - const queryClient = useQueryClient(); - const [connecting, setConnecting] = useState(false); - const pollTimerRef = useRef | null>(null); - const pollTimeoutRef = useRef | null>(null); - - const invalidateIntegrations = useCallback(() => { - void queryClient.invalidateQueries({ - queryKey: ["integrations", "list"], - }); - }, [queryClient]); - - const stopPolling = useCallback(() => { - if (pollTimerRef.current) { - clearInterval(pollTimerRef.current); - pollTimerRef.current = null; - } - if (pollTimeoutRef.current) { - clearTimeout(pollTimeoutRef.current); - pollTimeoutRef.current = null; - } - }, []); - - useEffect(() => stopPolling, [stopPolling]); - - useEffect(() => { - if (hasGithubIntegration && connecting) { - stopPolling(); - setConnecting(false); - } - }, [hasGithubIntegration, connecting, stopPolling]); - - // Fallback for when the `posthog-code://integration` deep link from PostHog Cloud - // never makes it back to the app (browser blocked the protocol prompt, focus didn't - // return cleanly, etc.). The integrations query has a 5-minute staleTime so the - // global `refetchOnWindowFocus: true` won't refetch it on its own — invalidate - // explicitly while a connect flow is in flight. - useEffect(() => { - if (!connecting) return; - const handleFocus = () => invalidateIntegrations(); - window.addEventListener("focus", handleFocus); - return () => window.removeEventListener("focus", handleFocus); - }, [connecting, invalidateIntegrations]); - - useGitHubIntegrationCallback({ - onSuccess: () => { - stopPolling(); - setConnecting(false); - invalidateIntegrations(); - }, - onError: ({ message }) => { - stopPolling(); - setConnecting(false); - toast.error(message); - }, - onTimedOut: () => { - stopPolling(); - setConnecting(false); - }, - }); - - const handleConnect = useCallback(async () => { - if (!cloudRegion || !projectId) return; - setConnecting(true); - try { - await trpcClient.githubIntegration.startFlow.mutate({ - region: cloudRegion, - projectId, - }); - - if (IS_DEV) { - pollTimerRef.current = setInterval(() => { - invalidateIntegrations(); - }, POLL_INTERVAL_MS); - } - - pollTimeoutRef.current = setTimeout(() => { - stopPolling(); - setConnecting(false); - }, POLL_TIMEOUT_MS); - } catch { - setConnecting(false); - } - }, [cloudRegion, projectId, invalidateIntegrations, stopPolling]); + const { + error: connectError, + isConnecting: connecting, + isTimedOut: timedOut, + hasError: hasConnectError, + connect: handleConnect, + } = useGithubUserConnect({ projectId }); return ( @@ -149,10 +67,20 @@ export function GitHubIntegrationSection({ ) : ( - + {hasGithubIntegration ? "Connected and active" - : "Required for the Inbox pipeline to work"} + : hasConnectError + ? describeGithubConnectError(connectError) + : timedOut + ? "We didn't hear back from GitHub. Try again." + : "Required for the Inbox pipeline to work"} )} @@ -173,7 +101,7 @@ export function GitHubIntegrationSection({ ) : ( )} diff --git a/apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx b/apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx index 105e61f86..028ab1e96 100644 --- a/apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx @@ -53,9 +53,13 @@ export function GitHubSettings() { const { reposByInstallationId, failedInstallationIds, isLoadingRepos } = useUserRepositoryIntegration(); - const { state, error, connect, reset } = useGithubUserConnect({ projectId }); - const isConnecting = state === "connecting"; - const hasConnectError = state === "error"; + const { + error, + isConnecting, + hasError: hasConnectError, + connect, + reset, + } = useGithubUserConnect({ projectId }); const canConnect = projectId != null && cloudRegion != null && !isConnecting; const handleConnect = () => { diff --git a/apps/code/src/renderer/features/task-detail/components/CloudGithubMissingNotice.tsx b/apps/code/src/renderer/features/task-detail/components/CloudGithubMissingNotice.tsx index c3d71725c..2f4fe832c 100644 --- a/apps/code/src/renderer/features/task-detail/components/CloudGithubMissingNotice.tsx +++ b/apps/code/src/renderer/features/task-detail/components/CloudGithubMissingNotice.tsx @@ -9,10 +9,8 @@ import { Button, Callout, Flex, Spinner, Text } from "@radix-ui/themes"; export function CloudGithubMissingNotice() { const projectId = useAuthStateValue((s) => s.projectId); const cloudRegion = useAuthStateValue((s) => s.cloudRegion); - const { state, error, connect, reset } = useGithubUserConnect({ projectId }); - - const isConnecting = state === "connecting"; - const hasError = state === "error"; + const { error, isConnecting, hasError, connect, reset } = + useGithubUserConnect({ projectId }); const canConnect = projectId != null && cloudRegion != null; return (