Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,7 @@ export function GitHubRepoPicker({
<Flex align="center" gap="2" style={{ minWidth: 0 }}>
<GithubLogo size={16} weight="regular" style={{ flexShrink: 0 }} />
<Text size={size} truncate>
{value
? value.includes("/")
? value.split("/").pop()
: value
: placeholder}
{value ?? placeholder}
</Text>
</Flex>
</Combobox.Trigger>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);
const [loading, setLoading] = useState(false);
const [connecting, setConnecting] = useState(false);
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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 {
Expand All @@ -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"),
},
Expand All @@ -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 (
<SetupFormContainer title="Connect GitHub">
<Flex direction="column" gap="3">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
});

Expand All @@ -281,7 +283,8 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) {
runCloudTask,
invalidateTasks,
navigateToTask,
githubIntegration?.id,
selectedRepo,
getIntegrationIdForRepo,
report.id,
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IntegrationStore>((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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(
null,
Expand All @@ -98,8 +98,12 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) {
>("local");
const [selectedModel, setSelectedModel] = useState<string | null>(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;

Expand All @@ -123,7 +127,7 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) {
editorRef,
selectedDirectory,
selectedRepository,
githubIntegrationId: githubIntegration?.id,
githubIntegrationId: selectedIntegrationId,
workspaceMode,
branch: selectedBranch,
editorIsEmpty,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(
() => lastUsedCloudRepository?.toLowerCase() ?? null,
Expand All @@ -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;

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

useEffect(() => {
if (
isLoadingRepos ||
!githubIntegration ||
!selectedRepository ||
selectedCloudRepository
) {
if (isLoadingRepos || !selectedRepository || selectedCloudRepository) {
return;
}

Expand All @@ -198,7 +197,6 @@ export function TaskInput({
setLastUsedCloudRepository(null);
}
}, [
githubIntegration,
isLoadingRepos,
lastUsedCloudRepository,
selectedCloudRepository,
Expand Down Expand Up @@ -264,7 +262,7 @@ export function TaskInput({
editorRef,
selectedDirectory,
selectedRepository: selectedCloudRepository,
githubIntegrationId: githubIntegration?.id,
githubIntegrationId: selectedIntegrationId,
workspaceMode: effectiveWorkspaceMode,
branch: branchForTaskCreation,
editorIsEmpty,
Expand Down
82 changes: 54 additions & 28 deletions apps/code/src/renderer/hooks/useIntegrations.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<string, number> = {};
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(
Expand All @@ -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,
};
}
Loading