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,
};
}