From 2b4e596ab0832147591951aaba7d5eaad92b24de Mon Sep 17 00:00:00 2001 From: Vojta Bartos Date: Tue, 5 May 2026 12:07:26 +0200 Subject: [PATCH 1/2] feat(code): GitHub settings page and cloud-mode missing-integration warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new "GitHub" settings category that lists every personal GitHub installation linked to the user's account, each individually disconnectable, with a single Connect / Connect-another button. Adds an amber callout below the task input in cloud mode when the user has no personal GitHub integration, prompting them to connect. The cloud repo picker stays user-only — no team-integration fallback — so PRs are always authored as the user. Generated-By: PostHog Code Task-Id: 955e2a44-cbb3-4ab8-9ba4-21deae8ffe19 --- .../hooks/useGithubUserConnect.ts | 2 +- .../settings/components/SettingsDialog.tsx | 5 + .../components/sections/GitHubSettings.tsx | 339 ++++++++++++++++++ .../settings/stores/settingsDialogStore.ts | 1 + .../components/CloudGithubMissingNotice.tsx | 54 +++ .../task-detail/components/TaskInput.tsx | 9 + 6 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx create mode 100644 apps/code/src/renderer/features/task-detail/components/CloudGithubMissingNotice.tsx diff --git a/apps/code/src/renderer/features/integrations/hooks/useGithubUserConnect.ts b/apps/code/src/renderer/features/integrations/hooks/useGithubUserConnect.ts index 5eeeb2945..3a51b71d9 100644 --- a/apps/code/src/renderer/features/integrations/hooks/useGithubUserConnect.ts +++ b/apps/code/src/renderer/features/integrations/hooks/useGithubUserConnect.ts @@ -86,7 +86,7 @@ export function invalidateGithubQueries( void queryClient.invalidateQueries({ queryKey: ["github_login"] }); } -async function openUrlInBrowser(url: string): Promise { +export async function openUrlInBrowser(url: string): Promise { try { await trpcClient.os.openExternal.mutate({ url }); } catch { diff --git a/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx b/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx index 0911dd08a..0f4ee8269 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, @@ -36,6 +37,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 { PersonalizationSettings } from "./sections/PersonalizationSettings"; import { PlanUsageSettings } from "./sections/PlanUsageSettings"; import { ShortcutsSettings } from "./sections/ShortcutsSettings"; @@ -73,6 +75,7 @@ const SIDEBAR_ITEMS: SidebarItem[] = [ }, { id: "claude-code", label: "Claude Code", icon: }, { id: "shortcuts", label: "Shortcuts", icon: }, + { id: "github", label: "GitHub", icon: }, { id: "signals", @@ -93,6 +96,7 @@ const CATEGORY_TITLES: Record = { personalization: "Personalization", "claude-code": "Claude Code", shortcuts: "Shortcuts", + github: "GitHub", signals: "Signals", updates: "Updates", @@ -109,6 +113,7 @@ const CATEGORY_COMPONENTS: Record = { personalization: PersonalizationSettings, "claude-code": ClaudeCodeSettings, 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..733b4590d --- /dev/null +++ b/apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx @@ -0,0 +1,339 @@ +import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient"; +import { useAuthStateValue } from "@features/auth/hooks/authQueries"; +import { + describeGithubConnectError, + invalidateGithubQueries, + openUrlInBrowser, + useGithubUserConnect, +} from "@features/integrations/hooks/useGithubUserConnect"; +import { + useUserGithubIntegrations, + useUserRepositoryIntegration, +} from "@hooks/useIntegrations"; +import { + ArrowSquareOutIcon, + CaretDownIcon, + CaretRightIcon, + GearSixIcon, + GitBranchIcon, + GithubLogoIcon, + WarningIcon, +} from "@phosphor-icons/react"; +import { + AlertDialog, + Box, + Button, + Flex, + IconButton, + Spinner, + Text, + Tooltip, +} from "@radix-ui/themes"; +import type { UserGitHubIntegration } from "@renderer/api/posthogClient"; +import { formatRelativeTimeLong } from "@renderer/utils/time"; +import { toast } from "@renderer/utils/toast"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; + +const REPO_PREVIEW_COUNT = 3; + +function githubInstallationSettingsUrl(integration: UserGitHubIntegration) { + const accountType = integration.account?.type?.toLowerCase(); + const accountName = integration.account?.name; + if (accountType === "organization" && accountName) { + return `https://github.com/organizations/${accountName}/settings/installations/${integration.installation_id}`; + } + return `https://github.com/settings/installations/${integration.installation_id}`; +} + +export function GitHubSettings() { + const projectId = useAuthStateValue((s) => s.projectId); + const cloudRegion = useAuthStateValue((s) => s.cloudRegion); + const { data: integrations = [], isLoading } = useUserGithubIntegrations(); + const { reposByInstallationId, failedInstallationIds, isLoadingRepos } = + useUserRepositoryIntegration(); + + const { state, error, connect, reset } = useGithubUserConnect({ projectId }); + const isConnecting = state === "connecting"; + const hasConnectError = state === "error"; + const canConnect = projectId != null && cloudRegion != null && !isConnecting; + + const handleConnect = () => { + if (hasConnectError) reset(); + void connect(); + }; + + const connectButtonLabel = + integrations.length === 0 ? "Connect GitHub" : "Connect another account"; + + return ( + + + + Personal GitHub installations linked to your PostHog account. Cloud + task PRs are authored as you using these. + + + + + {hasConnectError && ( + + {describeGithubConnectError(error)} + + )} + + + {isLoading ? ( + + + Loading… + + ) : integrations.length === 0 ? ( + + + + + + No GitHub integrations yet. Connect one to enable cloud tasks. + + + ) : ( + integrations.map((integration) => ( + + )) + )} + + + ); +} + +interface GitHubIntegrationRowProps { + integration: UserGitHubIntegration; + repos: string[]; + hasRepoFetchFailed: boolean; + isLoadingRepos: boolean; +} + +function GitHubIntegrationRow({ + integration, + repos, + hasRepoFetchFailed, + isLoadingRepos, +}: GitHubIntegrationRowProps) { + const apiClient = useOptionalAuthenticatedClient(); + const projectId = useAuthStateValue((s) => s.projectId); + const queryClient = useQueryClient(); + const [confirmOpen, setConfirmOpen] = useState(false); + const [expanded, setExpanded] = useState(false); + + const disconnect = useMutation({ + mutationFn: async () => { + if (!apiClient) throw new Error("Not authenticated"); + await apiClient.disconnectGithubUserIntegration( + integration.installation_id, + ); + }, + onSuccess: () => { + setConfirmOpen(false); + toast.success("Disconnected GitHub account"); + invalidateGithubQueries(queryClient, projectId); + }, + onError: (err) => { + toast.error( + err instanceof Error ? err.message : "Failed to disconnect GitHub", + ); + }, + }); + + const accountName = integration.account?.name?.trim() || "GitHub account"; + const repoCount = repos.length; + const canExpand = repoCount > 0; + const settingsUrl = githubInstallationSettingsUrl(integration); + + const repoPreview = repos.slice(0, REPO_PREVIEW_COUNT).join(", "); + const repoRemainder = repoCount - REPO_PREVIEW_COUNT; + + const repoSummaryNode = isLoadingRepos ? ( + Loading repositories… + ) : hasRepoFetchFailed ? ( + + + + Couldn't load repositories + + + ) : repoCount === 0 ? ( + + No repositories accessible + + ) : ( + + {repoCount} {repoCount === 1 ? "repository" : "repositories"} accessible:{" "} + {repoPreview} + {repoRemainder > 0 ? ` and ${repoRemainder} more` : ""} + + ); + + return ( + <> + + + + + + + + + Connected to{" "} + + + {integration.created_at && ( + + Created {formatRelativeTimeLong(integration.created_at)} + + )} + + + + {canExpand ? ( + + ) : ( + repoSummaryNode + )} + + + void openUrlInBrowser(settingsUrl)} + className="shrink-0" + > + + + + + + + + + {expanded && canExpand && ( +
+ + {repos.map((repo) => ( + + {repo} + + ))} + +
+ )} +
+ + { + if (!disconnect.isPending) setConfirmOpen(open); + }} + > + + + Disconnect {accountName}? + + + + You won't be able to create cloud tasks against repos in this + installation until you reconnect. + + + + + + + + + + + + ); +} diff --git a/apps/code/src/renderer/features/settings/stores/settingsDialogStore.ts b/apps/code/src/renderer/features/settings/stores/settingsDialogStore.ts index 6e15a18c6..b17317569 100644 --- a/apps/code/src/renderer/features/settings/stores/settingsDialogStore.ts +++ b/apps/code/src/renderer/features/settings/stores/settingsDialogStore.ts @@ -10,6 +10,7 @@ export type SettingsCategory = | "personalization" | "claude-code" | "shortcuts" + | "github" | "signals" | "updates" | "advanced"; diff --git a/apps/code/src/renderer/features/task-detail/components/CloudGithubMissingNotice.tsx b/apps/code/src/renderer/features/task-detail/components/CloudGithubMissingNotice.tsx new file mode 100644 index 000000000..a81493949 --- /dev/null +++ b/apps/code/src/renderer/features/task-detail/components/CloudGithubMissingNotice.tsx @@ -0,0 +1,54 @@ +import { useAuthStateValue } from "@features/auth/hooks/authQueries"; +import { + describeGithubConnectError, + useGithubUserConnect, +} from "@features/integrations/hooks/useGithubUserConnect"; +import { ArrowSquareOutIcon, InfoIcon } from "@phosphor-icons/react"; +import { Button, Callout, Flex, Spinner, Text } from "@radix-ui/themes"; + +export function CloudGithubMissingNotice() { + const projectId = useAuthStateValue((s) => s.projectId); + const cloudRegion = useAuthStateValue((s) => s.cloudRegion); + const { state, error, connect, reset } = useGithubUserConnect({ projectId }); + + const isConnecting = state === "connecting"; + const hasError = state === "error"; + const canConnect = projectId != null && cloudRegion != null; + + return ( + + + + + + + + + {hasError + ? describeGithubConnectError(error) + : "Connect your personal GitHub to create cloud tasks. Cloud PRs are authored as you."} + + + + + + + ); +} 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 80cf5e094..1590b396a 100644 --- a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx @@ -45,6 +45,7 @@ import { FOCUSABLE_SELECTOR } from "@utils/overlay"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { usePreviewConfig } from "../hooks/usePreviewConfig"; import { useTaskCreation } from "../hooks/useTaskCreation"; +import { CloudGithubMissingNotice } from "./CloudGithubMissingNotice"; import { type WorkspaceMode, WorkspaceModeSelect } from "./WorkspaceModeSelect"; interface TaskInputProps { @@ -182,6 +183,7 @@ export function TaskInput({ isLoadingRepos, isRefreshingRepos, refreshRepositories, + hasGithubIntegration, } = useUserRepositoryIntegration(); const { repositories: visibleCloudRepositories, @@ -779,6 +781,13 @@ export function TaskInput({ )} + {effectiveWorkspaceMode === "cloud" && + !isLoadingRepos && + !hasGithubIntegration && ( +
+ +
+ )} From 5ae26548d27d2b181d7671a5b9134c5ae68b169b Mon Sep 17 00:00:00 2001 From: Vojta Bartos Date: Tue, 5 May 2026 14:19:06 +0200 Subject: [PATCH 2/2] chore(code): tighten GitHub settings/notice copy Generated-By: PostHog Code Task-Id: 955e2a44-cbb3-4ab8-9ba4-21deae8ffe19 --- .../features/settings/components/sections/GitHubSettings.tsx | 3 +-- .../task-detail/components/CloudGithubMissingNotice.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx b/apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx index 733b4590d..105e61f86 100644 --- a/apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx @@ -70,8 +70,7 @@ export function GitHubSettings() { - Personal GitHub installations linked to your PostHog account. Cloud - task PRs are authored as you using these. + Personal GitHub installations linked to your PostHog account.