Skip to content
Open
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
10 changes: 8 additions & 2 deletions apps/code/src/main/services/oauth/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions apps/code/src/renderer/api/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ScenePersonalisationBasic>;
Expand Down Expand Up @@ -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<ScenePersonalisationBasic>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -475,6 +476,8 @@ export function InboxSignalsTab() {
</Flex>
</ScrollArea>

<GitHubConnectionBanner />

{/* Resize handle */}
<Box
onMouseDown={handleResizeMouseDown}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import { useDraftStore } from "@features/message-editor/stores/draftStore";
import { useCreateTask } from "@features/tasks/hooks/useTasks";
import { useFeatureFlag } from "@hooks/useFeatureFlag";
import { useRepositoryIntegration } from "@hooks/useIntegrations";
import { useMeQuery } from "@hooks/useMeQuery";
import {
ArrowSquareOutIcon,
CaretDownIcon,
CaretRightIcon,
ClockIcon,
Cloud as CloudIcon,
CommandIcon,
EyeIcon,
GithubLogoIcon,
KeyReturnIcon,
WarningIcon,
Expand All @@ -40,6 +42,7 @@ import type {
SignalReport,
SignalReportArtefact,
SignalReportArtefactsResponse,
SuggestedReviewer,
SuggestedReviewersArtefact,
} from "@shared/types";
import { useNavigationStore } from "@stores/navigationStore";
Expand All @@ -58,6 +61,20 @@ import { SignalReportSummaryMarkdown } from "../utils/SignalReportSummaryMarkdow
import { ReportTaskLogs } from "./ReportTaskLogs";
import { SignalCard } from "./SignalCard";

function isSuggestedReviewerRowMe(
reviewer: SuggestedReviewer,
me: { uuid: string; github_login: string | null } | undefined | null,
): boolean {
if (!me) return false;
if (reviewer.user?.uuid && me.uuid === reviewer.user.uuid) return true;
if (me.github_login && reviewer.github_login) {
return (
me.github_login.toLowerCase() === reviewer.github_login.toLowerCase()
);
}
return false;
}

// ── Helpers ─────────────────────────────────────────────────────────────────

function getArtefactsUnavailableMessage(
Expand Down Expand Up @@ -140,6 +157,7 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) {
// ── Auth / URLs ─────────────────────────────────────────────────────────
const cloudRegion = useAuthStateValue((state) => state.cloudRegion);
const projectId = useAuthStateValue((state) => state.projectId);
const { data: me } = useMeQuery();
const replayBaseUrl =
cloudRegion && projectId
? `${getCloudUrlFromRegion(cloudRegion)}/project/${projectId}/replay`
Expand Down Expand Up @@ -449,52 +467,69 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) {
Suggested reviewers
</Text>
<Flex direction="column" gap="1">
{suggestedReviewers.map((reviewer) => (
<Flex
key={reviewer.github_login}
align="center"
gap="2"
wrap="wrap"
>
<GithubLogoIcon
size={14}
className="shrink-0 text-gray-10"
/>
<Text size="1" className="text-[12px]">
{reviewer.user?.first_name ??
reviewer.github_name ??
reviewer.github_login}
</Text>
<a
href={`https://github.com/${reviewer.github_login}`}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-0.5 text-[11px] text-gray-9 hover:text-gray-11"
{suggestedReviewers.map((reviewer) => {
const isMe = isSuggestedReviewerRowMe(reviewer, me);
return (
<Flex
key={reviewer.github_login}
align="center"
gap="2"
wrap="wrap"
>
@{reviewer.github_login}
<ArrowSquareOutIcon size={10} />
</a>
{reviewer.relevant_commits.length > 0 && (
<span className="text-[11px] text-gray-9">
{reviewer.relevant_commits.map((commit, i) => (
<span key={commit.sha}>
{i > 0 && ", "}
<Tooltip content={commit.reason || undefined}>
<a
href={commit.url}
target="_blank"
rel="noreferrer"
className="font-mono text-gray-9 hover:text-gray-11"
>
{commit.sha.slice(0, 7)}
</a>
</Tooltip>
<GithubLogoIcon
size={14}
className="shrink-0 text-gray-10"
/>
<Text size="1" className="text-[12px]">
{reviewer.user?.first_name ??
reviewer.github_name ??
reviewer.github_login}
</Text>
{isMe && (
<Tooltip content="You are a suggested reviewer">
<span
className="inline-flex shrink-0 items-center rounded-sm px-1 py-px"
style={{
color: "var(--amber-11)",
backgroundColor: "var(--amber-3)",
border: "1px solid var(--amber-6)",
}}
>
<EyeIcon size={10} weight="bold" />
</span>
))}
</span>
)}
</Flex>
))}
</Tooltip>
)}
<a
href={`https://github.com/${reviewer.github_login}`}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-0.5 text-[11px] text-gray-9 hover:text-gray-11"
>
@{reviewer.github_login}
<ArrowSquareOutIcon size={10} />
</a>
{reviewer.relevant_commits.length > 0 && (
<span className="text-[11px] text-gray-9">
{reviewer.relevant_commits.map((commit, i) => (
<span key={commit.sha}>
{i > 0 && ", "}
<Tooltip content={commit.reason || undefined}>
<a
href={commit.url}
target="_blank"
rel="noreferrer"
className="font-mono text-gray-9 hover:text-gray-11"
>
{commit.sha.slice(0, 7)}
</a>
</Tooltip>
</span>
))}
</span>
)}
</Flex>
);
})}
</Flex>
</Box>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Button } from "@components/ui/Button";
import { useAuthStateValue } from "@features/auth/hooks/authQueries";
import { useMeQuery } from "@hooks/useMeQuery";
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() {
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.

i tried this in dev and the auth flow worked, but the banner never went away -- maybe expected cuz dev?

however, why do we need to show this always?

  • we should already have the user's github details, twice actually (once from signals onboarding, another from gh setup)
  • and for clean-UI-reasons i think we should hide this if we already have their details

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

On the global comment (threads are much better!): Great catch, didn't realize the background there is translucent. Fixing.

As for the banner not disappearing, I think I know why – we only recheck on app focus, and without any polling, so that can get clunky. I'll fix it so that after clicking we go into a "Waiting for auth to complete" state that also gently polls.

We do hide this after we have their details!

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.

nice, thank you!

how do we detect details for hiding it? it was there when i opened in dev, and i'm pretty sure i have the github integration set up on the project (i was using team 2) and definitely have gh auth'd

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ah, I'm just dumb – I didn't push it :picard: Basically when you have GitHub auth linked → banner = null.

Copy link
Copy Markdown
Member Author

@Twixes Twixes Apr 9, 2026

Choose a reason for hiding this comment

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

I missed your first point:

we should already have the user's github details

This is not the case because connecting your repo(s) and connecting your account are two different things. I would love to combine them, but they're distinct at the API level. In fact one is served by our GitHub GitHub app, while the other by our GitHub OAuth app (makes sense, right).

CleanShot 2026-04-09 at 19 39 59@2x

I've tweaked the wording on the new button's tooltip to make that clearer.

Copy link
Copy Markdown
Contributor

@adboio adboio Apr 9, 2026

Choose a reason for hiding this comment

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

ah i see… does this means we potentially have 3 different github auth flows? github app, oauth app, gh cli?

if so, i think we need to make that much easier

can we do one of these things?

  • combined auth flow for oauth + github app (i believe i saw this is possible?) then gh cli is separate (in this case we can also stop relying so heavily on gh cli in general, which would be excellent!)
  • or, have this new banner try to read user data from the gh cli first, so this oauth flow is not necessary in most cases? we already have a pretty hard dependency on the gh cli in posthog code, so it should almost always be available

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.

sorry this is claude slop and i will dig in further lol, but sharing in case this makes any sense to you as i'm sure you're more familiar with the gh auth flows:

  1. Build a GitHub App (not a plain OAuth App).
  2. Use the GitHub App's user authorization flow to get a user-to-server token — this covers the "OAuth" side (acting on behalf of the user).
  3. Use the app's installation access token (JWT → installation token exchange) for app-level operations that don't need a user context.

You don't get both tokens from one redirect, but the user only sees one authorization prompt (the GitHub App authorization). You then separately mint the installation token server-side using your app's private key — no additional user interaction required.

const { data: user, isLoading } = useMeQuery();
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: ["me"] });
}
};
window.addEventListener("focus", onFocus);
return () => window.removeEventListener("focus", onFocus);
}, []);

if (isLoading || !user) {
return null;
}

if (user.github_login) {
return null;
}

if (!cloudRegion) {
return null;
}

const connectUrl = posthogCloudGithubAccountLinkUrl(cloudRegion);

return (
<div className="pointer-events-auto absolute inset-x-2 bottom-2 z-20">
<Button
size="1"
variant="solid"
color="gray"
highContrast
className="h-fit w-full flex-wrap items-center justify-start gap-x-2 gap-y-1 whitespace-normal border-transparent bg-black py-1 text-left text-[12px] text-white leading-tight shadow-none hover:bg-neutral-900"
tooltipContent={
<>
<InfoIcon size={14} className="mr-0.5" />
<div>
PostHog Code suggests report ownership using cutting-edge{" "}
<code>git blame</code> technology.
<br />
For this, connect your GitHub profile (different from connecting
repositories).
</div>
</>
}
onClick={() => {
awaitingLink.current = true;
void trpcClient.os.openExternal.mutate({ url: connectUrl });
}}
>
<GithubLogoIcon className="flex-none" size={12} />
<span className="min-w-0 flex-1 basis-0">
{`Connect your GitHub profile to highlight what's relevant to you`}
</span>
<ArrowSquareOutIcon className="flex-none" size={11} />
</Button>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ export function ReportCardContent({
<span
className="inline-flex shrink-0 items-center rounded-sm px-1 py-px"
style={{
color: "var(--blue-11)",
backgroundColor: "var(--blue-3)",
border: "1px solid var(--blue-6)",
color: "var(--amber-11)",
backgroundColor: "var(--amber-3)",
border: "1px solid var(--amber-6)",
}}
>
<EyeIcon size={10} weight="bold" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading