Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.
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 @@ -110,8 +110,12 @@ export function GitIntegrationStep({
isLoading: githubUserIntegrationsLoading,
} = useUserGithubIntegrations();
const hasGitIntegration = githubUserIntegrations.length > 0;
const { repositories, isLoadingRepos } = useUserRepositoryIntegration();
const { repositories, isLoadingRepos, failedInstallationIds } =
useUserRepositoryIntegration();
const githubIntegration = githubUserIntegrations[0] ?? null;
const integrationIsStale =
!!githubIntegration &&
failedInstallationIds.includes(githubIntegration.installation_id);

const alternativeConnectedProjects = useMemo(() => {
if (hasGitIntegration) return [];
Expand Down Expand Up @@ -149,17 +153,18 @@ export function GitIntegrationStep({
const apiClient = useOptionalAuthenticatedClient();
const [disconnectConfirmOpen, setDisconnectConfirmOpen] = useState(false);
const disconnectMutation = useMutation({
mutationFn: async () => {
mutationFn: async (opts: { silent?: boolean } = {}) => {
const installationId = githubIntegration?.installation_id;
if (!apiClient || !installationId) {
throw new Error("No GitHub integration to disconnect");
}
await apiClient.disconnectGithubUserIntegration(installationId);
return { silent: opts.silent ?? false };
},
onSuccess: () => {
onSuccess: ({ silent }) => {
setDisconnectConfirmOpen(false);
invalidateGithubQueries(queryClient, selectedProjectId);
toast.success("GitHub disconnected.");
if (!silent) toast.success("GitHub disconnected.");
},
onError: (e) => {
toast.error(
Expand Down Expand Up @@ -339,28 +344,65 @@ export function GitIntegrationStep({
{isLoading || githubUserIntegrationsLoading ? (
<Skeleton className="h-[16px] w-[80px]" />
) : hasGitIntegration ? (
<Flex align="center" gap="1">
<CheckCircle
size={14}
weight="fill"
className="text-(--green-9)"
/>
<Text className="text-(--green-11) text-[13px]">
Connected
integrationIsStale ? (
<Text className="text-(--amber-11) text-[13px]">
Reconnect needed
</Text>
</Flex>
) : (
<Flex align="center" gap="1">
<CheckCircle
size={14}
weight="fill"
className="text-(--green-9)"
/>
<Text className="text-(--green-11) text-[13px]">
Connected
</Text>
</Flex>
)
) : null}
</Flex>
{hasGitIntegration ? (
<Flex direction="column" gap="3">
<Text className="text-(--gray-11) text-sm">
{isLoadingRepos
? "Loading repositories..."
: repoSummary
? `Access to ${repoSummary}`
: "No repositories found. Check your GitHub app settings."}
<Text
className={
integrationIsStale
? "text-(--amber-11) text-sm"
: "text-(--gray-11) text-sm"
}
>
{integrationIsStale
? "We can't reach GitHub with your saved authorization. The app may have been uninstalled or the token revoked. Reconnect to restore access."
: isLoadingRepos
? "Loading repositories..."
: repoSummary
? `Access to ${repoSummary}`
: "No repositories found. Check your GitHub app settings."}
</Text>
<Flex align="center" gap="3">
<Flex align="center" gap="3" wrap="wrap">
{integrationIsStale && (
<Button
size="1"
variant="solid"
loading={
disconnectMutation.isPending || isConnecting
}
onClick={async () => {
if (!githubIntegration?.installation_id) return;
try {
await disconnectMutation.mutateAsync({
silent: true,
});
} catch {
return;
}
void handleConnectGitHub();
}}
Comment on lines +390 to +400

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent disconnect failure may leave dangling integration

If disconnectMutation.mutateAsync fails (e.g. a 5xx from the backend), the error is swallowed and handleConnectGitHub() is still called. The old stale integration record remains in the database while a new OAuth flow starts. Depending on whether the backend enforces uniqueness per user, this can result in two integration rows, and invalidateGithubQueries is never called (since onSuccess doesn't fire), so the cache retains the stale entry until the reconnect completes.

Consider invalidating the relevant queries in the catch path so the UI doesn't remain in an inconsistent state even when disconnect fails.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx
Line: 390-396

Comment:
**Silent disconnect failure may leave dangling integration**

If `disconnectMutation.mutateAsync` fails (e.g. a 5xx from the backend), the error is swallowed and `handleConnectGitHub()` is still called. The old stale integration record remains in the database while a new OAuth flow starts. Depending on whether the backend enforces uniqueness per user, this can result in two integration rows, and `invalidateGithubQueries` is never called (since `onSuccess` doesn't fire), so the cache retains the stale entry until the reconnect completes.

Consider invalidating the relevant queries in the catch path so the UI doesn't remain in an inconsistent state even when disconnect fails.

How can I resolve this? If you propose a fix, please make it concise.

>
Reconnect
<ArrowSquareOut size={12} />
</Button>
)}
<Button
size="1"
variant="soft"
Expand Down Expand Up @@ -595,7 +637,7 @@ export function GitIntegrationStep({
<Button
variant="solid"
color="red"
onClick={() => disconnectMutation.mutate()}
onClick={() => disconnectMutation.mutate({})}
disabled={disconnectMutation.isPending}
>
{disconnectMutation.isPending ? <Spinner size="1" /> : null}
Expand Down
26 changes: 20 additions & 6 deletions apps/code/src/renderer/hooks/useIntegrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,16 @@ function useAllUserGithubRepositories(
})),
combine: (results) => {
const map: Record<string, UserRepositoryIntegrationRef> = {};
const failedInstallationIds: string[] = [];
let pending = false;
for (const result of results) {
results.forEach((result, index) => {
if (result.isPending) pending = true;
if (!result.data) continue;
if (result.isError) {
const installationId =
githubIntegrations[index]?.installation_id ?? null;
if (installationId) failedInstallationIds.push(installationId);
Comment on lines +155 to +158

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Stale detection triggers on any error, not just auth failures

Any error that survives React Query's retry budget (network timeouts, 5xx server errors, etc.) will add the installation to failedInstallationIds and show "Reconnect needed" to the user, even when the GitHub token itself is still valid. This could prompt unnecessary reconnects during a temporary backend outage.

If the API client surfaces HTTP status codes, consider restricting the stale signal to 401/403 responses only.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/hooks/useIntegrations.ts
Line: 155-158

Comment:
**Stale detection triggers on any error, not just auth failures**

Any error that survives React Query's retry budget (network timeouts, 5xx server errors, etc.) will add the installation to `failedInstallationIds` and show "Reconnect needed" to the user, even when the GitHub token itself is still valid. This could prompt unnecessary reconnects during a temporary backend outage.

If the API client surfaces HTTP status codes, consider restricting the stale signal to 401/403 responses only.

How can I resolve this? If you propose a fix, please make it concise.

}
if (!result.data) return;
for (const repo of result.data.repos ?? []) {
if (!(repo in map)) {
map[repo] = {
Expand All @@ -160,8 +166,12 @@ function useAllUserGithubRepositories(
};
}
}
}
return { repositoryMap: map, isPending: pending };
});
return {
repositoryMap: map,
isPending: pending,
failedInstallationIds,
};
},
});
}
Expand Down Expand Up @@ -489,8 +499,11 @@ export function useUserRepositoryIntegration() {
useUserGithubIntegrations();
const [isRefreshingRepos, setIsRefreshingRepos] = useState(false);

const { repositoryMap, isPending: reposPending } =
useAllUserGithubRepositories(githubIntegrations);
const {
repositoryMap,
isPending: reposPending,
failedInstallationIds,
} = useAllUserGithubRepositories(githubIntegrations);

const repositories = useMemo(
() => Object.keys(repositoryMap),
Expand Down Expand Up @@ -555,6 +568,7 @@ export function useUserRepositoryIntegration() {
isRefreshingRepos,
refreshRepositories,
hasGithubIntegration: githubIntegrations.length > 0,
failedInstallationIds,
};
}

Expand Down
Loading