Skip to content

Commit d092804

Browse files
chore(code): support multiple gh integrations in cloud repo picker (#1586)
1 parent e6ae536 commit d092804

7 files changed

Lines changed: 96 additions & 73 deletions

File tree

apps/code/src/renderer/features/folder-picker/components/GitHubRepoPicker.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,7 @@ export function GitHubRepoPicker({
5454
<Flex align="center" gap="2" style={{ minWidth: 0 }}>
5555
<GithubLogo size={16} weight="regular" style={{ flexShrink: 0 }} />
5656
<Text size={size} truncate>
57-
{value
58-
? value.includes("/")
59-
? value.split("/").pop()
60-
: value
61-
: placeholder}
57+
{value ?? placeholder}
6258
</Text>
6359
</Flex>
6460
</Combobox.Trigger>

apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,12 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
5959
const projectId = useAuthStateValue((state) => state.projectId);
6060
const cloudRegion = useAuthStateValue((state) => state.cloudRegion);
6161
const client = useAuthenticatedClient();
62-
const { githubIntegration, repositories, isLoadingRepos } =
63-
useRepositoryIntegration();
62+
const {
63+
repositories,
64+
getIntegrationIdForRepo,
65+
isLoadingRepos,
66+
hasGithubIntegration,
67+
} = useRepositoryIntegration();
6468
const [repo, setRepo] = useState<string | null>(null);
6569
const [loading, setLoading] = useState(false);
6670
const [connecting, setConnecting] = useState(false);
@@ -82,11 +86,11 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
8286

8387
// Stop polling once integration appears
8488
useEffect(() => {
85-
if (githubIntegration && connecting) {
89+
if (hasGithubIntegration && connecting) {
8690
stopPolling();
8791
setConnecting(false);
8892
}
89-
}, [githubIntegration, connecting, stopPolling]);
93+
}, [hasGithubIntegration, connecting, stopPolling]);
9094

9195
// Auto-select the first repo once loaded
9296
useEffect(() => {
@@ -137,7 +141,10 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
137141
}, [cloudRegion, projectId, client, stopPolling]);
138142

139143
const handleSubmit = useCallback(async () => {
140-
if (!projectId || !client || !repo || !githubIntegration) return;
144+
const githubIntegrationId = repo
145+
? getIntegrationIdForRepo(repo)
146+
: undefined;
147+
if (!projectId || !client || !repo || !githubIntegrationId) return;
141148

142149
setLoading(true);
143150
try {
@@ -147,7 +154,7 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
147154
repository: repo,
148155
auth_method: {
149156
selection: "oauth",
150-
github_integration_id: githubIntegration.id,
157+
github_integration_id: githubIntegrationId,
151158
},
152159
schemas: schemasPayload("github"),
153160
},
@@ -161,9 +168,9 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
161168
} finally {
162169
setLoading(false);
163170
}
164-
}, [projectId, client, repo, githubIntegration, onComplete]);
171+
}, [projectId, client, repo, getIntegrationIdForRepo, onComplete]);
165172

166-
if (!githubIntegration) {
173+
if (!hasGithubIntegration) {
167174
return (
168175
<SetupFormContainer title="Connect GitHub">
169176
<Flex direction="column" gap="3">

apps/code/src/renderer/features/inbox/components/detail/ReportDetailPane.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) {
209209
const { navigateToTaskInput, navigateToTask } = useNavigationStore();
210210
const draftActions = useDraftStore((s) => s.actions);
211211
const { invalidateTasks } = useCreateTask();
212-
const { githubIntegration, repositories } = useRepositoryIntegration();
212+
const { repositories, getIntegrationIdForRepo } = useRepositoryIntegration();
213213
const cloudModeEnabled = useFeatureFlag("twig-cloud-mode-toggle");
214214

215215
const isRunningCloudTask = useInboxCloudTaskStore((s) => s.isRunning);
@@ -265,7 +265,9 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) {
265265

266266
const result = await runCloudTask({
267267
prompt,
268-
githubIntegrationId: githubIntegration?.id,
268+
githubIntegrationId: selectedRepo
269+
? getIntegrationIdForRepo(selectedRepo)
270+
: undefined,
269271
reportId: report.id,
270272
});
271273

@@ -281,7 +283,8 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) {
281283
runCloudTask,
282284
invalidateTasks,
283285
navigateToTask,
284-
githubIntegration?.id,
286+
selectedRepo,
287+
getIntegrationIdForRepo,
285288
report.id,
286289
]);
287290

apps/code/src/renderer/features/integrations/stores/integrationStore.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,25 @@ export interface Integration {
88

99
interface IntegrationStore {
1010
integrations: Integration[];
11-
repositories: string[];
1211
setIntegrations: (integrations: Integration[]) => void;
13-
setRepositories: (repositories: string[]) => void;
1412
}
1513

1614
interface IntegrationSelectors {
17-
githubIntegration: Integration | undefined;
18-
isRepoInIntegration: (repoKey: string) => boolean;
15+
githubIntegrations: Integration[];
16+
hasGithubIntegration: boolean;
1917
}
2018

2119
export const useIntegrationStore = create<IntegrationStore>((set) => ({
2220
integrations: [],
23-
repositories: [],
2421
setIntegrations: (integrations) => set({ integrations }),
25-
setRepositories: (repositories) => set({ repositories }),
2622
}));
2723

2824
export const useIntegrationSelectors = (): IntegrationSelectors => {
2925
const integrations = useIntegrationStore((state) => state.integrations);
30-
const repositories = useIntegrationStore((state) => state.repositories);
31-
32-
const githubIntegration = integrations.find((i) => i.kind === "github");
33-
34-
const isRepoInIntegration = (repoKey: string) => {
35-
if (!repoKey) return true;
36-
return repositories.some((r) => r === repoKey.toLowerCase());
37-
};
26+
const githubIntegrations = integrations.filter((i) => i.kind === "github");
3827

3928
return {
40-
githubIntegration,
41-
isRepoInIntegration,
29+
githubIntegrations,
30+
hasGithubIntegration: githubIntegrations.length > 0,
4231
};
4332
};

apps/code/src/renderer/features/onboarding/components/TutorialStep.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) {
8585
}, []);
8686

8787
// GitHub repos
88-
const { githubIntegration, repositories, isLoadingRepos } =
88+
const { repositories, getIntegrationIdForRepo, isLoadingRepos } =
8989
useRepositoryIntegration();
9090
const [selectedRepository, setSelectedRepository] = useState<string | null>(
9191
null,
@@ -98,8 +98,12 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) {
9898
>("local");
9999
const [selectedModel, setSelectedModel] = useState<string | null>(null);
100100

101+
const selectedIntegrationId = selectedRepository
102+
? getIntegrationIdForRepo(selectedRepository)
103+
: undefined;
104+
101105
const { data: cloudBranchData, isPending: cloudBranchesLoading } =
102-
useGithubBranches(githubIntegration?.id, selectedRepository);
106+
useGithubBranches(selectedIntegrationId, selectedRepository);
103107
const cloudBranches = cloudBranchData?.branches;
104108
const cloudDefaultBranch = cloudBranchData?.defaultBranch ?? null;
105109

@@ -123,7 +127,7 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) {
123127
editorRef,
124128
selectedDirectory,
125129
selectedRepository,
126-
githubIntegrationId: githubIntegration?.id,
130+
githubIntegrationId: selectedIntegrationId,
127131
workspaceMode,
128132
branch: selectedBranch,
129133
editorIsEmpty,

apps/code/src/renderer/features/task-detail/components/TaskInput.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export function TaskInput({
102102
const setAdapter = (newAdapter: AgentAdapter) =>
103103
setLastUsedAdapter(newAdapter);
104104

105-
const { githubIntegration, repositories, isLoadingRepos } =
105+
const { repositories, getIntegrationIdForRepo, isLoadingRepos } =
106106
useRepositoryIntegration();
107107
const [selectedRepository, setSelectedRepository] = useState<string | null>(
108108
() => lastUsedCloudRepository?.toLowerCase() ?? null,
@@ -115,8 +115,12 @@ export function TaskInput({
115115
const { currentBranch, branchLoading, defaultBranch } =
116116
useGitQueries(selectedDirectory);
117117

118+
const selectedIntegrationId = selectedCloudRepository
119+
? getIntegrationIdForRepo(selectedCloudRepository)
120+
: undefined;
121+
118122
const { data: cloudBranchData, isPending: cloudBranchesLoading } =
119-
useGithubBranches(githubIntegration?.id, selectedCloudRepository);
123+
useGithubBranches(selectedIntegrationId, selectedCloudRepository);
120124
const cloudBranches = cloudBranchData?.branches;
121125
const cloudDefaultBranch = cloudBranchData?.defaultBranch ?? null;
122126

@@ -184,12 +188,7 @@ export function TaskInput({
184188
}, [lastUsedCloudRepository, selectedRepository]);
185189

186190
useEffect(() => {
187-
if (
188-
isLoadingRepos ||
189-
!githubIntegration ||
190-
!selectedRepository ||
191-
selectedCloudRepository
192-
) {
191+
if (isLoadingRepos || !selectedRepository || selectedCloudRepository) {
193192
return;
194193
}
195194

@@ -198,7 +197,6 @@ export function TaskInput({
198197
setLastUsedCloudRepository(null);
199198
}
200199
}, [
201-
githubIntegration,
202200
isLoadingRepos,
203201
lastUsedCloudRepository,
204202
selectedCloudRepository,
@@ -264,7 +262,7 @@ export function TaskInput({
264262
editorRef,
265263
selectedDirectory,
266264
selectedRepository: selectedCloudRepository,
267-
githubIntegrationId: githubIntegration?.id,
265+
githubIntegrationId: selectedIntegrationId,
268266
workspaceMode: effectiveWorkspaceMode,
269267
branch: branchForTaskCreation,
270268
editorIsEmpty,

apps/code/src/renderer/hooks/useIntegrations.ts

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1+
import { useAuthenticatedClient } from "@features/auth/hooks/authClient";
2+
import { AUTH_SCOPED_QUERY_META } from "@features/auth/hooks/authQueries";
13
import {
4+
type Integration,
25
useIntegrationSelectors,
36
useIntegrationStore,
47
} from "@features/integrations/stores/integrationStore";
5-
import { useEffect } from "react";
8+
import { useQueries } from "@tanstack/react-query";
9+
import { useCallback, useEffect, useMemo } from "react";
610
import { useAuthenticatedQuery } from "./useAuthenticatedQuery";
711

8-
interface Integration {
9-
id: number;
10-
kind: string;
11-
[key: string]: unknown;
12-
}
13-
1412
const integrationKeys = {
1513
all: ["integrations"] as const,
1614
list: () => [...integrationKeys.all, "list"] as const,
@@ -37,24 +35,36 @@ export function useIntegrations() {
3735
return query;
3836
}
3937

40-
function useRepositories(integrationId?: number) {
41-
const setRepositories = useIntegrationStore((state) => state.setRepositories);
38+
function useAllGithubRepositories(githubIntegrations: Integration[]) {
39+
const client = useAuthenticatedClient();
4240

43-
const query = useAuthenticatedQuery(
44-
integrationKeys.repositories(integrationId),
45-
async (client) => {
46-
if (!integrationId) return [];
47-
return await client.getGithubRepositories(integrationId);
41+
return useQueries({
42+
queries: githubIntegrations.map((integration) => ({
43+
queryKey: integrationKeys.repositories(integration.id),
44+
queryFn: async () => {
45+
if (!client) throw new Error("Not authenticated");
46+
const repos = await client.getGithubRepositories(integration.id);
47+
return { integrationId: integration.id, repos };
48+
},
49+
enabled: !!client,
50+
staleTime: 5 * 60 * 1000,
51+
meta: AUTH_SCOPED_QUERY_META,
52+
})),
53+
combine: (results) => {
54+
const map: Record<string, number> = {};
55+
let pending = false;
56+
for (const result of results) {
57+
if (result.isPending) pending = true;
58+
if (!result.data) continue;
59+
for (const repo of result.data.repos) {
60+
if (!(repo in map)) {
61+
map[repo] = result.data.integrationId;
62+
}
63+
}
64+
}
65+
return { repositoryMap: map, isPending: pending };
4866
},
49-
);
50-
51-
useEffect(() => {
52-
if (query.data) {
53-
setRepositories(query.data);
54-
}
55-
}, [query.data, setRepositories]);
56-
57-
return query;
67+
});
5868
}
5969

6070
export function useGithubBranches(
@@ -73,16 +83,32 @@ export function useGithubBranches(
7383

7484
export function useRepositoryIntegration() {
7585
const { isPending: integrationsPending } = useIntegrations();
76-
const { githubIntegration } = useIntegrationSelectors();
77-
const { isPending: reposPending } = useRepositories(githubIntegration?.id);
86+
const { githubIntegrations, hasGithubIntegration } =
87+
useIntegrationSelectors();
88+
89+
const { repositoryMap, isPending: reposPending } =
90+
useAllGithubRepositories(githubIntegrations);
91+
92+
const repositories = useMemo(
93+
() => Object.keys(repositoryMap),
94+
[repositoryMap],
95+
);
96+
97+
const getIntegrationIdForRepo = useCallback(
98+
(repoKey: string) => repositoryMap[repoKey?.toLowerCase()],
99+
[repositoryMap],
100+
);
78101

79-
const repositories = useIntegrationStore((state) => state.repositories);
80-
const { isRepoInIntegration } = useIntegrationSelectors();
102+
const isRepoInIntegration = useCallback(
103+
(repoKey: string) => !repoKey || repoKey.toLowerCase() in repositoryMap,
104+
[repositoryMap],
105+
);
81106

82107
return {
83-
githubIntegration,
84108
repositories,
109+
getIntegrationIdForRepo,
85110
isRepoInIntegration,
86111
isLoadingRepos: integrationsPending || reposPending,
112+
hasGithubIntegration,
87113
};
88114
}

0 commit comments

Comments
 (0)