diff --git a/apps/code/src/main/services/oauth/service.ts b/apps/code/src/main/services/oauth/service.ts index 661072c71..501d5396b 100644 --- a/apps/code/src/main/services/oauth/service.ts +++ b/apps/code/src/main/services/oauth/service.ts @@ -10,6 +10,7 @@ import { shell } from "electron"; import { inject, injectable } from "inversify"; import { MAIN_TOKENS } from "../../di/tokens"; import { logger } from "../../utils/logger"; +import { focusMainWindow } from "../../window"; import type { DeepLinkService } from "../deep-link/service"; import type { CancelFlowOutput, @@ -63,8 +64,13 @@ export class OAuthService { const error = searchParams.get("error"); if (!this.pendingFlow) { - log.warn("Received OAuth callback but no pending flow"); - return false; + // Same deep link as desktop sign-in (`posthog-code://callback`), but auth finished in + // the browser (e.g. GitHub on PostHog Cloud) — refocus so the user lands back in Code. + log.info( + "OAuth callback deep link with no in-app flow — refocusing (e.g. return from web auth)", + ); + focusMainWindow("oauth callback deep link (no in-app flow)"); + return true; } const { resolve, reject, timeoutId } = this.pendingFlow; diff --git a/apps/code/src/renderer/api/generated.ts b/apps/code/src/renderer/api/generated.ts index 3ff508285..8be8e0a15 100644 --- a/apps/code/src/renderer/api/generated.ts +++ b/apps/code/src/renderer/api/generated.ts @@ -10906,6 +10906,7 @@ export namespace Schemas { events_column_config?: unknown | undefined; is_2fa_enabled: boolean; has_social_auth: boolean; + github_login: string | null; has_sso_enforcement: boolean; has_seen_product_intro_for?: null | undefined; scene_personalisation: Array; @@ -12487,6 +12488,7 @@ export namespace Schemas { events_column_config: unknown; is_2fa_enabled: boolean; has_social_auth: boolean; + github_login: string | null; has_sso_enforcement: boolean; has_seen_product_intro_for: null; scene_personalisation: Array; diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index 913308d60..b356638b7 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -382,6 +382,14 @@ export class PostHogAPIClient { return data; } + async getGithubLogin(): Promise { + // @ts-expect-error this is not in the generated client YET + const data = (await this.api.get("/api/users/{uuid}/github_login/", { + path: { uuid: "@me" }, + })) as { github_login: string | null }; + return data.github_login; + } + async switchOrganization(orgId: string): Promise { await this.api.patch("/api/users/{uuid}/", { path: { uuid: "@me" }, diff --git a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx index e8c7e5b5d..33fd26406 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx @@ -29,6 +29,7 @@ import { useRendererWindowFocusStore } from "@stores/rendererWindowFocusStore"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { MultiSelectStack } from "./detail/MultiSelectStack"; import { ReportDetailPane } from "./detail/ReportDetailPane"; +import { GitHubConnectionBanner } from "./list/GitHubConnectionBanner"; import { ReportListPane } from "./list/ReportListPane"; import { SignalsToolbar } from "./list/SignalsToolbar"; @@ -475,6 +476,8 @@ export function InboxSignalsTab() { + + {/* Resize handle */} state.cloudRegion); const projectId = useAuthStateValue((state) => state.projectId); + const { data: me } = useMeQuery(); const replayBaseUrl = cloudRegion && projectId ? `${getCloudUrlFromRegion(cloudRegion)}/project/${projectId}/replay` @@ -449,52 +460,69 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) { Suggested reviewers - {suggestedReviewers.map((reviewer) => ( - - - - {reviewer.user?.first_name ?? - reviewer.github_name ?? - reviewer.github_login} - - { + const isMe = isSuggestedReviewerRowMe(reviewer, me?.uuid); + return ( + - @{reviewer.github_login} - - - {reviewer.relevant_commits.length > 0 && ( - - {reviewer.relevant_commits.map((commit, i) => ( - - {i > 0 && ", "} - - - {commit.sha.slice(0, 7)} - - + + + {reviewer.user?.first_name ?? + reviewer.github_name ?? + reviewer.github_login} + + {isMe && ( + + + - ))} - - )} - - ))} + + )} + + @{reviewer.github_login} + + + {reviewer.relevant_commits.length > 0 && ( + + {reviewer.relevant_commits.map((commit, i) => ( + + {i > 0 && ", "} + + + {commit.sha.slice(0, 7)} + + + + ))} + + )} + + ); + })} )} diff --git a/apps/code/src/renderer/features/inbox/components/list/GitHubConnectionBanner.tsx b/apps/code/src/renderer/features/inbox/components/list/GitHubConnectionBanner.tsx new file mode 100644 index 000000000..c813c71ac --- /dev/null +++ b/apps/code/src/renderer/features/inbox/components/list/GitHubConnectionBanner.tsx @@ -0,0 +1,90 @@ +import { Button } from "@components/ui/Button"; +import { useAuthStateValue } from "@features/auth/hooks/authQueries"; +import { useAuthenticatedQuery } from "@hooks/useAuthenticatedQuery"; +import { + ArrowSquareOutIcon, + GithubLogoIcon, + InfoIcon, +} from "@phosphor-icons/react"; +import { trpcClient } from "@renderer/trpc/client"; +import { getCloudUrlFromRegion } from "@shared/constants/oauth"; +import type { CloudRegion } from "@shared/types/oauth"; +import { queryClient } from "@utils/queryClient"; +import { useEffect, useRef } from "react"; + +/** PostHog Cloud OAuth URL to attach GitHub (`connect_from` is handled by PostHog web after redirect). */ +function posthogCloudGithubAccountLinkUrl(region: CloudRegion): string { + const url = new URL("/login/github/", getCloudUrlFromRegion(region)); + url.searchParams.set("connect_from", "posthog_code"); + return url.toString(); +} + +export function GitHubConnectionBanner() { + const { data: githubLogin, isLoading } = useAuthenticatedQuery( + ["github_login"], + async (client) => client.getGithubLogin(), + { staleTime: 5 * 60 * 1000 }, + ); + const cloudRegion = useAuthStateValue((s) => s.cloudRegion); + const awaitingLink = useRef(false); + + // After the user clicks connect and returns to the app, refetch to pick up the new github_login + useEffect(() => { + const onFocus = () => { + if (awaitingLink.current) { + awaitingLink.current = false; + void queryClient.invalidateQueries({ queryKey: ["github_login"] }); + } + }; + window.addEventListener("focus", onFocus); + return () => window.removeEventListener("focus", onFocus); + }, []); + + if (isLoading) { + return null; + } + + if (githubLogin) { + return null; + } + + if (!cloudRegion) { + return null; + } + + const connectUrl = posthogCloudGithubAccountLinkUrl(cloudRegion); + + return ( +
+ +
+ ); +} diff --git a/apps/code/src/renderer/features/inbox/components/list/ReportListRow.tsx b/apps/code/src/renderer/features/inbox/components/list/ReportListRow.tsx index 045f1ebe4..5c2b66700 100644 --- a/apps/code/src/renderer/features/inbox/components/list/ReportListRow.tsx +++ b/apps/code/src/renderer/features/inbox/components/list/ReportListRow.tsx @@ -40,7 +40,7 @@ export function ReportListRow({ const rowBgClass = isSelected ? "bg-gray-3" : report.is_suggested_reviewer - ? "bg-blue-2" + ? "bg-amber-2" : ""; const firstProduct = (report.source_products ?? [])[0]; diff --git a/apps/code/src/renderer/features/inbox/components/list/SignalsToolbar.tsx b/apps/code/src/renderer/features/inbox/components/list/SignalsToolbar.tsx index 5b0fa80cd..580c0ec3a 100644 --- a/apps/code/src/renderer/features/inbox/components/list/SignalsToolbar.tsx +++ b/apps/code/src/renderer/features/inbox/components/list/SignalsToolbar.tsx @@ -68,7 +68,7 @@ function formatPauseRemaining(pausedUntil: string): string { return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`; } -const inboxLivePollingTooltip = `Inbox refetches the report list about every ${(INBOX_REFETCH_INTERVAL_MS / 1000).toFixed(1)} seconds while this window is focused and Inbox is open. Refetching pauses when you switch to another app or navigate away from Inbox.`; +const inboxLivePollingTooltip = `Inbox is focused – syncing reports every ${(INBOX_REFETCH_INTERVAL_MS / 1000).toFixed(1)} s…`; export function SignalsToolbar({ totalCount, diff --git a/apps/code/src/renderer/features/inbox/components/utils/ReportCardContent.tsx b/apps/code/src/renderer/features/inbox/components/utils/ReportCardContent.tsx index 3ae4b67d4..7e07e6935 100644 --- a/apps/code/src/renderer/features/inbox/components/utils/ReportCardContent.tsx +++ b/apps/code/src/renderer/features/inbox/components/utils/ReportCardContent.tsx @@ -61,9 +61,9 @@ export function ReportCardContent({ diff --git a/apps/code/src/renderer/features/inbox/utils/inboxConstants.ts b/apps/code/src/renderer/features/inbox/utils/inboxConstants.ts index 0b82017fe..66f1d69b0 100644 --- a/apps/code/src/renderer/features/inbox/utils/inboxConstants.ts +++ b/apps/code/src/renderer/features/inbox/utils/inboxConstants.ts @@ -3,4 +3,4 @@ export const INBOX_PIPELINE_STATUS_FILTER = "potential,candidate,in_progress,ready,pending_input"; /** Polling interval for inbox queries while the Electron window is focused. */ -export const INBOX_REFETCH_INTERVAL_MS = 2800; +export const INBOX_REFETCH_INTERVAL_MS = 3000;