diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index b2003f114..be78c5166 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -622,6 +622,26 @@ export class PostHogAPIClient { return data.results ?? []; } + async disconnectGithubUserIntegration(installationId: string): Promise { + const urlPath = `/api/users/@me/integrations/github/${installationId}/`; + const url = new URL(`${this.api.baseUrl}${urlPath}`); + const response = await this.api.fetcher.fetch({ + method: "delete", + url, + path: urlPath, + }); + if (!response.ok && response.status !== 204) { + const err = (await response.json().catch(() => ({}))) as { + detail?: unknown; + }; + const detail = + typeof err.detail === "string" + ? err.detail + : "Failed to disconnect GitHub"; + throw new Error(detail); + } + } + async switchOrganization(orgId: string): Promise { await this.api.patch("/api/users/{uuid}/", { path: { uuid: "@me" }, diff --git a/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx b/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx index 43ccb5914..efc2c5253 100644 --- a/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx +++ b/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx @@ -19,6 +19,7 @@ import { CreditCard, Folder, GearSix, + GithubLogo, HardDrives, Keyboard, Palette, @@ -37,6 +38,7 @@ import { ClaudeCodeSettings } from "./sections/ClaudeCodeSettings"; import { CloudEnvironmentsSettings } from "./sections/CloudEnvironmentsSettings"; import { EnvironmentsSettings } from "./sections/environments/EnvironmentsSettings"; import { GeneralSettings } from "./sections/GeneralSettings"; +import { GitHubSettings } from "./sections/GitHubSettings"; import { McpServersSettings } from "./sections/McpServersSettings"; import { PersonalizationSettings } from "./sections/PersonalizationSettings"; import { PlanUsageSettings } from "./sections/PlanUsageSettings"; @@ -82,6 +84,7 @@ const SIDEBAR_ITEMS: SidebarItem[] = [ fullwidth: true, }, { id: "shortcuts", label: "Shortcuts", icon: }, + { id: "github", label: "GitHub", icon: }, { id: "signals", @@ -103,6 +106,7 @@ const CATEGORY_TITLES: Record = { "claude-code": "Claude Code", "mcp-servers": "MCP Servers", shortcuts: "Shortcuts", + github: "GitHub", signals: "Signals", updates: "Updates", @@ -120,6 +124,7 @@ const CATEGORY_COMPONENTS: Record = { "claude-code": ClaudeCodeSettings, "mcp-servers": McpServersSettings, shortcuts: ShortcutsSettings, + github: GitHubSettings, signals: SignalSourcesSettings, updates: UpdatesSettings, diff --git a/apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx b/apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx new file mode 100644 index 000000000..d8747814f --- /dev/null +++ b/apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx @@ -0,0 +1,287 @@ +import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient"; +import { useGitHubIntegrationCallback } from "@features/integrations/hooks/useGitHubIntegrationCallback"; +import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore"; +import { useConnectUserGithub } from "@hooks/useConnectUserGithub"; +import { + useRepositoryIntegration, + useUserGithubIntegrations, +} from "@hooks/useIntegrations"; +import { + ArrowSquareOutIcon, + CheckCircleIcon, + GithubLogoIcon, + InfoIcon, +} from "@phosphor-icons/react"; +import { + AlertDialog, + Box, + Button, + Flex, + Spinner, + Text, + Tooltip, +} from "@radix-ui/themes"; +import { toast } from "@renderer/utils/toast"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; + +export function GitHubSettings() { + const apiClient = useOptionalAuthenticatedClient(); + const queryClient = useQueryClient(); + const { data: integrations = [], isLoading } = useUserGithubIntegrations(); + const integration = integrations[0]; + + const { connect, isConnecting, canConnect } = useConnectUserGithub(); + const [confirmDisconnect, setConfirmDisconnect] = useState(false); + + const setSettingsCategory = useSettingsDialogStore( + (state) => state.setCategory, + ); + const { + repositories: teamRepositories, + hasGithubIntegration: hasTeamIntegration, + isLoadingRepos: isLoadingTeam, + } = useRepositoryIntegration(); + + useGitHubIntegrationCallback({ + onSuccess: () => { + void queryClient.invalidateQueries({ + queryKey: ["user-github-integrations"], + }); + void queryClient.invalidateQueries({ + queryKey: ["integrations", "list"], + }); + }, + onError: (message) => { + toast.error(message); + }, + }); + + const disconnect = useMutation({ + mutationFn: async (installationId: string) => { + if (!apiClient) throw new Error("Not authenticated"); + await apiClient.disconnectGithubUserIntegration(installationId); + }, + onSuccess: async () => { + setConfirmDisconnect(false); + toast.success("Disconnected personal GitHub"); + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: ["user-github-integrations"], + }), + queryClient.invalidateQueries({ + queryKey: ["integrations", "list"], + }), + ]); + }, + onError: (error) => { + toast.error( + error instanceof Error ? error.message : "Failed to disconnect GitHub", + ); + }, + }); + + const accountName = integration?.account?.name?.trim() ?? null; + const isConnected = !!integration; + + return ( + + + + + + + + + Personal GitHub + + {isLoading ? ( + Loading… + ) : isConnected ? ( + + + + Connected{accountName ? ` as ${accountName}` : ""} · used for + cloud task PRs + + + ) : ( + + Connect your personal GitHub so cloud-task PRs are authored as + you. + + )} + + + + + {isConnecting ? ( + + + Waiting… + + ) : isConnected ? ( + <> + + + + ) : ( + + )} + + + + + + + + + + + Project GitHub + + {isLoadingTeam ? ( + Loading… + ) : hasTeamIntegration ? ( + teamRepositories.length > 0 ? ( + + {teamRepositories.map((repo) => ( + + {repo} + + ))} + + } + side="bottom" + > + + + + Connected · {teamRepositories.length}{" "} + {teamRepositories.length === 1 ? "repo" : "repos"} + + + + + ) : ( + + + + Connected + + + ) + ) : ( + + No project-level GitHub integration on this team. + + )} + + + + + + + + + { + if (!disconnect.isPending) setConfirmDisconnect(open); + }} + > + + + Disconnect personal GitHub? + + + + Cloud task PRs will fall back to your team's integration. You can + reconnect at any time. + + + + + + + + + + + + ); +} diff --git a/apps/code/src/renderer/features/settings/stores/settingsDialogStore.ts b/apps/code/src/renderer/features/settings/stores/settingsDialogStore.ts index 69660dded..b37c77698 100644 --- a/apps/code/src/renderer/features/settings/stores/settingsDialogStore.ts +++ b/apps/code/src/renderer/features/settings/stores/settingsDialogStore.ts @@ -11,6 +11,7 @@ export type SettingsCategory = | "claude-code" | "shortcuts" | "mcp-servers" + | "github" | "signals" | "updates" | "advanced";