diff --git a/apps/code/src/renderer/features/folder-picker/components/GitHubRepoPicker.tsx b/apps/code/src/renderer/features/folder-picker/components/GitHubRepoPicker.tsx index f36661350..d46370484 100644 --- a/apps/code/src/renderer/features/folder-picker/components/GitHubRepoPicker.tsx +++ b/apps/code/src/renderer/features/folder-picker/components/GitHubRepoPicker.tsx @@ -54,11 +54,7 @@ export function GitHubRepoPicker({ - {value - ? value.includes("/") - ? value.split("/").pop() - : value - : placeholder} + {value ?? placeholder} diff --git a/apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx b/apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx index fd2178ae0..f72c75828 100644 --- a/apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx +++ b/apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx @@ -59,8 +59,12 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) { const projectId = useAuthStateValue((state) => state.projectId); const cloudRegion = useAuthStateValue((state) => state.cloudRegion); const client = useAuthenticatedClient(); - const { githubIntegration, repositories, isLoadingRepos } = - useRepositoryIntegration(); + const { + repositories, + getIntegrationIdForRepo, + isLoadingRepos, + hasGithubIntegration, + } = useRepositoryIntegration(); const [repo, setRepo] = useState(null); const [loading, setLoading] = useState(false); const [connecting, setConnecting] = useState(false); @@ -82,11 +86,11 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) { // Stop polling once integration appears useEffect(() => { - if (githubIntegration && connecting) { + if (hasGithubIntegration && connecting) { stopPolling(); setConnecting(false); } - }, [githubIntegration, connecting, stopPolling]); + }, [hasGithubIntegration, connecting, stopPolling]); // Auto-select the first repo once loaded useEffect(() => { @@ -137,7 +141,10 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) { }, [cloudRegion, projectId, client, stopPolling]); const handleSubmit = useCallback(async () => { - if (!projectId || !client || !repo || !githubIntegration) return; + const githubIntegrationId = repo + ? getIntegrationIdForRepo(repo) + : undefined; + if (!projectId || !client || !repo || !githubIntegrationId) return; setLoading(true); try { @@ -147,7 +154,7 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) { repository: repo, auth_method: { selection: "oauth", - github_integration_id: githubIntegration.id, + github_integration_id: githubIntegrationId, }, schemas: schemasPayload("github"), }, @@ -161,9 +168,9 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) { } finally { setLoading(false); } - }, [projectId, client, repo, githubIntegration, onComplete]); + }, [projectId, client, repo, getIntegrationIdForRepo, onComplete]); - if (!githubIntegration) { + if (!hasGithubIntegration) { return ( diff --git a/apps/code/src/renderer/features/inbox/components/detail/ReportDetailPane.tsx b/apps/code/src/renderer/features/inbox/components/detail/ReportDetailPane.tsx index 6da0d7cfb..67c0a7287 100644 --- a/apps/code/src/renderer/features/inbox/components/detail/ReportDetailPane.tsx +++ b/apps/code/src/renderer/features/inbox/components/detail/ReportDetailPane.tsx @@ -209,7 +209,7 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) { const { navigateToTaskInput, navigateToTask } = useNavigationStore(); const draftActions = useDraftStore((s) => s.actions); const { invalidateTasks } = useCreateTask(); - const { githubIntegration, repositories } = useRepositoryIntegration(); + const { repositories, getIntegrationIdForRepo } = useRepositoryIntegration(); const cloudModeEnabled = useFeatureFlag("twig-cloud-mode-toggle"); const isRunningCloudTask = useInboxCloudTaskStore((s) => s.isRunning); @@ -265,7 +265,9 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) { const result = await runCloudTask({ prompt, - githubIntegrationId: githubIntegration?.id, + githubIntegrationId: selectedRepo + ? getIntegrationIdForRepo(selectedRepo) + : undefined, reportId: report.id, }); @@ -281,7 +283,8 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) { runCloudTask, invalidateTasks, navigateToTask, - githubIntegration?.id, + selectedRepo, + getIntegrationIdForRepo, report.id, ]); diff --git a/apps/code/src/renderer/features/integrations/stores/integrationStore.ts b/apps/code/src/renderer/features/integrations/stores/integrationStore.ts index f844ad706..38b1fabe8 100644 --- a/apps/code/src/renderer/features/integrations/stores/integrationStore.ts +++ b/apps/code/src/renderer/features/integrations/stores/integrationStore.ts @@ -8,36 +8,25 @@ export interface Integration { interface IntegrationStore { integrations: Integration[]; - repositories: string[]; setIntegrations: (integrations: Integration[]) => void; - setRepositories: (repositories: string[]) => void; } interface IntegrationSelectors { - githubIntegration: Integration | undefined; - isRepoInIntegration: (repoKey: string) => boolean; + githubIntegrations: Integration[]; + hasGithubIntegration: boolean; } export const useIntegrationStore = create((set) => ({ integrations: [], - repositories: [], setIntegrations: (integrations) => set({ integrations }), - setRepositories: (repositories) => set({ repositories }), })); export const useIntegrationSelectors = (): IntegrationSelectors => { const integrations = useIntegrationStore((state) => state.integrations); - const repositories = useIntegrationStore((state) => state.repositories); - - const githubIntegration = integrations.find((i) => i.kind === "github"); - - const isRepoInIntegration = (repoKey: string) => { - if (!repoKey) return true; - return repositories.some((r) => r === repoKey.toLowerCase()); - }; + const githubIntegrations = integrations.filter((i) => i.kind === "github"); return { - githubIntegration, - isRepoInIntegration, + githubIntegrations, + hasGithubIntegration: githubIntegrations.length > 0, }; }; diff --git a/apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx b/apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx index 77495339d..6e9bd4cb9 100644 --- a/apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx @@ -85,7 +85,7 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) { }, []); // GitHub repos - const { githubIntegration, repositories, isLoadingRepos } = + const { repositories, getIntegrationIdForRepo, isLoadingRepos } = useRepositoryIntegration(); const [selectedRepository, setSelectedRepository] = useState( null, @@ -98,8 +98,12 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) { >("local"); const [selectedModel, setSelectedModel] = useState(null); + const selectedIntegrationId = selectedRepository + ? getIntegrationIdForRepo(selectedRepository) + : undefined; + const { data: cloudBranchData, isPending: cloudBranchesLoading } = - useGithubBranches(githubIntegration?.id, selectedRepository); + useGithubBranches(selectedIntegrationId, selectedRepository); const cloudBranches = cloudBranchData?.branches; const cloudDefaultBranch = cloudBranchData?.defaultBranch ?? null; @@ -123,7 +127,7 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) { editorRef, selectedDirectory, selectedRepository, - githubIntegrationId: githubIntegration?.id, + githubIntegrationId: selectedIntegrationId, workspaceMode, branch: selectedBranch, editorIsEmpty, diff --git a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx index 45d85ccff..fde1e8f4a 100644 --- a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx @@ -102,7 +102,7 @@ export function TaskInput({ const setAdapter = (newAdapter: AgentAdapter) => setLastUsedAdapter(newAdapter); - const { githubIntegration, repositories, isLoadingRepos } = + const { repositories, getIntegrationIdForRepo, isLoadingRepos } = useRepositoryIntegration(); const [selectedRepository, setSelectedRepository] = useState( () => lastUsedCloudRepository?.toLowerCase() ?? null, @@ -115,8 +115,12 @@ export function TaskInput({ const { currentBranch, branchLoading, defaultBranch } = useGitQueries(selectedDirectory); + const selectedIntegrationId = selectedCloudRepository + ? getIntegrationIdForRepo(selectedCloudRepository) + : undefined; + const { data: cloudBranchData, isPending: cloudBranchesLoading } = - useGithubBranches(githubIntegration?.id, selectedCloudRepository); + useGithubBranches(selectedIntegrationId, selectedCloudRepository); const cloudBranches = cloudBranchData?.branches; const cloudDefaultBranch = cloudBranchData?.defaultBranch ?? null; @@ -184,12 +188,7 @@ export function TaskInput({ }, [lastUsedCloudRepository, selectedRepository]); useEffect(() => { - if ( - isLoadingRepos || - !githubIntegration || - !selectedRepository || - selectedCloudRepository - ) { + if (isLoadingRepos || !selectedRepository || selectedCloudRepository) { return; } @@ -198,7 +197,6 @@ export function TaskInput({ setLastUsedCloudRepository(null); } }, [ - githubIntegration, isLoadingRepos, lastUsedCloudRepository, selectedCloudRepository, @@ -264,7 +262,7 @@ export function TaskInput({ editorRef, selectedDirectory, selectedRepository: selectedCloudRepository, - githubIntegrationId: githubIntegration?.id, + githubIntegrationId: selectedIntegrationId, workspaceMode: effectiveWorkspaceMode, branch: branchForTaskCreation, editorIsEmpty, diff --git a/apps/code/src/renderer/hooks/useIntegrations.ts b/apps/code/src/renderer/hooks/useIntegrations.ts index f5890b18f..b69a0459b 100644 --- a/apps/code/src/renderer/hooks/useIntegrations.ts +++ b/apps/code/src/renderer/hooks/useIntegrations.ts @@ -1,16 +1,14 @@ +import { useAuthenticatedClient } from "@features/auth/hooks/authClient"; +import { AUTH_SCOPED_QUERY_META } from "@features/auth/hooks/authQueries"; import { + type Integration, useIntegrationSelectors, useIntegrationStore, } from "@features/integrations/stores/integrationStore"; -import { useEffect } from "react"; +import { useQueries } from "@tanstack/react-query"; +import { useCallback, useEffect, useMemo } from "react"; import { useAuthenticatedQuery } from "./useAuthenticatedQuery"; -interface Integration { - id: number; - kind: string; - [key: string]: unknown; -} - const integrationKeys = { all: ["integrations"] as const, list: () => [...integrationKeys.all, "list"] as const, @@ -37,24 +35,36 @@ export function useIntegrations() { return query; } -function useRepositories(integrationId?: number) { - const setRepositories = useIntegrationStore((state) => state.setRepositories); +function useAllGithubRepositories(githubIntegrations: Integration[]) { + const client = useAuthenticatedClient(); - const query = useAuthenticatedQuery( - integrationKeys.repositories(integrationId), - async (client) => { - if (!integrationId) return []; - return await client.getGithubRepositories(integrationId); + return useQueries({ + queries: githubIntegrations.map((integration) => ({ + queryKey: integrationKeys.repositories(integration.id), + queryFn: async () => { + if (!client) throw new Error("Not authenticated"); + const repos = await client.getGithubRepositories(integration.id); + return { integrationId: integration.id, repos }; + }, + enabled: !!client, + staleTime: 5 * 60 * 1000, + meta: AUTH_SCOPED_QUERY_META, + })), + combine: (results) => { + const map: Record = {}; + let pending = false; + for (const result of results) { + if (result.isPending) pending = true; + if (!result.data) continue; + for (const repo of result.data.repos) { + if (!(repo in map)) { + map[repo] = result.data.integrationId; + } + } + } + return { repositoryMap: map, isPending: pending }; }, - ); - - useEffect(() => { - if (query.data) { - setRepositories(query.data); - } - }, [query.data, setRepositories]); - - return query; + }); } export function useGithubBranches( @@ -73,16 +83,32 @@ export function useGithubBranches( export function useRepositoryIntegration() { const { isPending: integrationsPending } = useIntegrations(); - const { githubIntegration } = useIntegrationSelectors(); - const { isPending: reposPending } = useRepositories(githubIntegration?.id); + const { githubIntegrations, hasGithubIntegration } = + useIntegrationSelectors(); + + const { repositoryMap, isPending: reposPending } = + useAllGithubRepositories(githubIntegrations); + + const repositories = useMemo( + () => Object.keys(repositoryMap), + [repositoryMap], + ); + + const getIntegrationIdForRepo = useCallback( + (repoKey: string) => repositoryMap[repoKey?.toLowerCase()], + [repositoryMap], + ); - const repositories = useIntegrationStore((state) => state.repositories); - const { isRepoInIntegration } = useIntegrationSelectors(); + const isRepoInIntegration = useCallback( + (repoKey: string) => !repoKey || repoKey.toLowerCase() in repositoryMap, + [repositoryMap], + ); return { - githubIntegration, repositories, + getIntegrationIdForRepo, isRepoInIntegration, isLoadingRepos: integrationsPending || reposPending, + hasGithubIntegration, }; }