diff --git a/apps/code/src/main/services/git/schemas.ts b/apps/code/src/main/services/git/schemas.ts index 388e05ef2..4c5111087 100644 --- a/apps/code/src/main/services/git/schemas.ts +++ b/apps/code/src/main/services/git/schemas.ts @@ -212,6 +212,14 @@ export const commitInput = z.object({ export type CommitInput = z.infer; +// Git CLI status +export const gitStatusOutput = z.object({ + installed: z.boolean(), + version: z.string().nullable(), +}); + +export type GitStatusOutput = z.infer; + // GitHub CLI status export const ghStatusOutput = z.object({ installed: z.boolean(), diff --git a/apps/code/src/main/services/git/service.ts b/apps/code/src/main/services/git/service.ts index 03c63fca7..d9a158b90 100644 --- a/apps/code/src/main/services/git/service.ts +++ b/apps/code/src/main/services/git/service.ts @@ -1,5 +1,10 @@ +import { execFile } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + import { execGh } from "@posthog/git/gh"; import { getAllBranches, @@ -53,6 +58,7 @@ import type { GitHubIssue, GitRepoInfo, GitStateSnapshot, + GitStatusOutput, GitSyncStatus, OpenPrOutput, PrActionType, @@ -683,6 +689,16 @@ export class GitService extends TypedEventEmitter { }; } + public async getGitStatus(): Promise { + try { + const { stdout } = await execFileAsync("git", ["--version"]); + const version = stdout.trim().replace("git version ", ""); + return { installed: true, version }; + } catch { + return { installed: false, version: null }; + } + } + public async getGhStatus(): Promise { const versionResult = await execGh(["--version"]); if (versionResult.exitCode !== 0) { @@ -699,7 +715,9 @@ export class GitService extends TypedEventEmitter { const authResult = await execGh(["auth", "status"]); const authenticated = authResult.exitCode === 0; const authOutput = `${authResult.stdout}\n${authResult.stderr}`; - const usernameMatch = authOutput.match(/Logged in to github.com as (\S+)/); + const usernameMatch = authOutput.match( + /Logged in to github.com (?:as |account )(\S+)/, + ); return { installed: true, diff --git a/apps/code/src/main/trpc/routers/git.ts b/apps/code/src/main/trpc/routers/git.ts index 2f3d3baa2..d29d79e8a 100644 --- a/apps/code/src/main/trpc/routers/git.ts +++ b/apps/code/src/main/trpc/routers/git.ts @@ -51,6 +51,7 @@ import { ghAuthTokenOutput, ghStatusOutput, gitStateSnapshotSchema, + gitStatusOutput, openPrInput, openPrOutput, prStatusInput, @@ -269,6 +270,10 @@ export const gitRouter = router({ getService().sync(input.directoryPath, input.remote), ), + getGitStatus: publicProcedure + .output(gitStatusOutput) + .query(() => getService().getGitStatus()), + getGhStatus: publicProcedure .output(ghStatusOutput) .query(() => getService().getGhStatus()), diff --git a/apps/code/src/renderer/App.tsx b/apps/code/src/renderer/App.tsx index 650c38ab7..9b4e19cdf 100644 --- a/apps/code/src/renderer/App.tsx +++ b/apps/code/src/renderer/App.tsx @@ -137,6 +137,11 @@ function App() { }), ); + const needsInviteCode = + isAuthenticated && hasCodeAccess === false && hasCompletedOnboarding; + const isCheckingAccess = + isAuthenticated && hasCodeAccess === null && hasCompletedOnboarding; + // Handle transition into main app — only show the dark overlay if dark mode is active useEffect(() => { const isInMainApp = isAuthenticated && hasCompletedOnboarding; @@ -164,8 +169,16 @@ function App() { ); } - // Four-phase rendering: auth → access gate → onboarding → main app + // Rendering: onboarding (includes auth + invite code gate) → main app const renderContent = () => { + if (!hasCompletedOnboarding) { + return ( + + + + ); + } + if (!isAuthenticated) { return ( @@ -174,10 +187,9 @@ function App() { ); } - // Access check loading state - if (hasCodeAccess === null) { + if (isCheckingAccess) { return ( - + @@ -188,23 +200,14 @@ function App() { ); } - // Access gate: show invite code screen if flag is not enabled - if (!hasCodeAccess) { + if (needsInviteCode) { return ( - + ); } - if (!hasCompletedOnboarding) { - return ( - - - - ); - } - return ( { if (typeof repo === "string") return repo; return (repo.full_name ?? repo.name ?? "").toLowerCase(); diff --git a/apps/code/src/renderer/assets/images/bw-logo.png b/apps/code/src/renderer/assets/images/bw-logo.png deleted file mode 100644 index c17130774..000000000 Binary files a/apps/code/src/renderer/assets/images/bw-logo.png and /dev/null differ diff --git a/apps/code/src/renderer/assets/images/cave-hero.jpg b/apps/code/src/renderer/assets/images/cave-hero.jpg deleted file mode 100644 index 05e58d269..000000000 Binary files a/apps/code/src/renderer/assets/images/cave-hero.jpg and /dev/null differ diff --git a/apps/code/src/renderer/assets/images/hedgehogs/builder-hog-03.png b/apps/code/src/renderer/assets/images/hedgehogs/builder-hog-03.png new file mode 100644 index 000000000..f935563f3 Binary files /dev/null and b/apps/code/src/renderer/assets/images/hedgehogs/builder-hog-03.png differ diff --git a/apps/code/src/renderer/assets/images/hedgehogs/clickthat-hog.png b/apps/code/src/renderer/assets/images/hedgehogs/clickthat-hog.png new file mode 100644 index 000000000..d07a36187 Binary files /dev/null and b/apps/code/src/renderer/assets/images/hedgehogs/clickthat-hog.png differ diff --git a/apps/code/src/renderer/assets/images/hedgehogs/detective-hog.png b/apps/code/src/renderer/assets/images/hedgehogs/detective-hog.png new file mode 100644 index 000000000..4ba2b7499 Binary files /dev/null and b/apps/code/src/renderer/assets/images/hedgehogs/detective-hog.png differ diff --git a/apps/code/src/renderer/assets/images/explorer-hog.png b/apps/code/src/renderer/assets/images/hedgehogs/explorer-hog.png similarity index 100% rename from apps/code/src/renderer/assets/images/explorer-hog.png rename to apps/code/src/renderer/assets/images/hedgehogs/explorer-hog.png diff --git a/apps/code/src/renderer/assets/images/hedgehogs/feature-flag-hog.png b/apps/code/src/renderer/assets/images/hedgehogs/feature-flag-hog.png new file mode 100644 index 000000000..efdfafe05 Binary files /dev/null and b/apps/code/src/renderer/assets/images/hedgehogs/feature-flag-hog.png differ diff --git a/apps/code/src/renderer/assets/images/graphs-hog.png b/apps/code/src/renderer/assets/images/hedgehogs/graphs-hog.png similarity index 100% rename from apps/code/src/renderer/assets/images/graphs-hog.png rename to apps/code/src/renderer/assets/images/hedgehogs/graphs-hog.png diff --git a/apps/code/src/renderer/assets/images/hedgehogs/happy-hog.png b/apps/code/src/renderer/assets/images/hedgehogs/happy-hog.png new file mode 100644 index 000000000..854cc44d5 Binary files /dev/null and b/apps/code/src/renderer/assets/images/hedgehogs/happy-hog.png differ diff --git a/apps/code/src/renderer/assets/images/logomark.svg b/apps/code/src/renderer/assets/images/logomark.svg deleted file mode 100644 index ebd58692e..000000000 --- a/apps/code/src/renderer/assets/images/logomark.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/apps/code/src/renderer/assets/images/tree-bg.svg b/apps/code/src/renderer/assets/images/tree-bg.svg deleted file mode 100644 index bb0fc4ac1..000000000 --- a/apps/code/src/renderer/assets/images/tree-bg.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/apps/code/src/renderer/assets/images/wordmark-white.svg b/apps/code/src/renderer/assets/images/wordmark-white.svg new file mode 100644 index 000000000..08c1e772d --- /dev/null +++ b/apps/code/src/renderer/assets/images/wordmark-white.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/code/src/renderer/components/DotPatternBackground.tsx b/apps/code/src/renderer/components/DotPatternBackground.tsx new file mode 100644 index 000000000..5c8ae0433 --- /dev/null +++ b/apps/code/src/renderer/components/DotPatternBackground.tsx @@ -0,0 +1,45 @@ +import { useId } from "react"; + +const DOT_FILL = "var(--gray-6)"; + +interface DotPatternBackgroundProps { + style?: React.CSSProperties; +} + +export function DotPatternBackground({ style }: DotPatternBackgroundProps) { + const patternId = useId(); + + return ( + + ); +} diff --git a/apps/code/src/renderer/components/FullScreenLayout.tsx b/apps/code/src/renderer/components/FullScreenLayout.tsx new file mode 100644 index 000000000..a30297a8e --- /dev/null +++ b/apps/code/src/renderer/components/FullScreenLayout.tsx @@ -0,0 +1,111 @@ +import { Lifebuoy } from "@phosphor-icons/react"; +import { Button, Flex, Theme } from "@radix-ui/themes"; +import phWordmark from "@renderer/assets/images/wordmark.svg"; +import phWordmarkWhite from "@renderer/assets/images/wordmark-white.svg"; +import { trpcClient } from "@renderer/trpc/client"; +import { useThemeStore } from "@stores/themeStore"; +import { EXTERNAL_LINKS } from "@utils/links"; +import type { ReactNode } from "react"; +import { DotPatternBackground } from "./DotPatternBackground"; +import { DraggableTitleBar } from "./DraggableTitleBar"; + +interface FullScreenLayoutProps { + children: ReactNode; + footerLeft?: ReactNode; + footerRight?: ReactNode; +} + +export function FullScreenLayout({ + children, + footerLeft, + footerRight, +}: FullScreenLayoutProps) { + const isDarkMode = useThemeStore((state) => state.isDarkMode); + + return ( + + + + +
+ + + + PostHog + + + {children} + + + + {footerLeft ?? ( + + )} + {footerRight ??
} + + + + + ); +} diff --git a/apps/code/src/renderer/components/KeyboardShortcutsSheet.tsx b/apps/code/src/renderer/components/KeyboardShortcutsSheet.tsx index 4a7e9f79a..0699109aa 100644 --- a/apps/code/src/renderer/components/KeyboardShortcutsSheet.tsx +++ b/apps/code/src/renderer/components/KeyboardShortcutsSheet.tsx @@ -1,14 +1,60 @@ -import { Box, Dialog, Flex, Kbd, Text } from "@radix-ui/themes"; +import { Box, Dialog, Flex, Text } from "@radix-ui/themes"; import { CATEGORY_LABELS, - formatHotkey, + formatHotkeyParts, getShortcutsByCategory, type ShortcutCategory, } from "@renderer/constants/keyboard-shortcuts"; -import { isMac } from "@utils/platform"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; +function Keycap({ label, size = "md" }: { label: string; size?: "sm" | "md" }) { + const [pressed, setPressed] = useState(false); + const isSmall = size === "sm"; + const minW = isSmall ? "22px" : "28px"; + const h = isSmall ? "22px" : "28px"; + const fontSize = isSmall ? "11px" : "13px"; + const shadowSize = isSmall ? "2px" : "3px"; + + return ( + // biome-ignore lint/a11y/noStaticElementInteractions: cosmetic press animation + setPressed(true)} + onMouseUp={() => setPressed(false)} + onMouseLeave={() => setPressed(false)} + style={{ + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + minWidth: minW, + height: h, + padding: "0 6px", + fontSize, + fontFamily: "system-ui, -apple-system, sans-serif", + fontWeight: 500, + lineHeight: 1, + color: "var(--gray-11)", + backgroundColor: "var(--gray-3)", + border: "1px solid var(--gray-5)", + borderBottomWidth: pressed ? "1px" : shadowSize, + borderBottomColor: "var(--gray-7)", + borderRadius: "6px", + transform: pressed + ? `translateY(${isSmall ? "1px" : "2px"})` + : "translateY(0)", + transition: + "transform 80ms ease-out, border-bottom-width 80ms ease-out", + userSelect: "none", + boxSizing: "border-box", + cursor: "pointer", + }} + > + {label} + + ); +} + interface KeyboardShortcutsSheetProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -32,37 +78,57 @@ export function KeyboardShortcutsSheet({ style={{ maxHeight: "80vh", overflow: "hidden" }} onEscapeKeyDown={(e) => e.preventDefault()} > - - Keyboard Shortcuts - + + + + - - - - onOpenChange(false)} - > - Press Esc to close - - - ); } +function ShortcutsHeader() { + const triggerParts = formatHotkeyParts("mod+/"); + + return ( + + + + Keyboard Combos + + + {triggerParts.map((part) => ( + + ))} + + + + Your cheat codes for shipping faster + + + ); +} + export function KeyboardShortcutsList() { const shortcutsByCategory = useMemo(() => getShortcutsByCategory(), []); @@ -121,14 +187,7 @@ export function KeyboardShortcutsList() { index % 2 === 0 ? "var(--gray-2)" : "var(--gray-1)", }} > - - {shortcut.description} - {shortcut.context && ( - - {shortcut.context} - - )} - + {shortcut.description} - {formatted} - - ); - } + const parts = formatHotkeyParts(keys); - const keyParts = formatted.split("+"); return ( - {keyParts.map((part) => ( - - {part} - + {parts.map((part) => ( + ))} ); diff --git a/apps/code/src/renderer/constants/keyboard-shortcuts.ts b/apps/code/src/renderer/constants/keyboard-shortcuts.ts index 26fb45464..a4329c3e4 100644 --- a/apps/code/src/renderer/constants/keyboard-shortcuts.ts +++ b/apps/code/src/renderer/constants/keyboard-shortcuts.ts @@ -48,13 +48,6 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [ description: "Open command menu", category: "general", }, - { - id: "toggle-focus", - keys: SHORTCUTS.TOGGLE_FOCUS, - description: "Toggle focus mode", - category: "general", - context: "Worktree task", - }, { id: "settings", keys: SHORTCUTS.SETTINGS, @@ -219,31 +212,39 @@ export function getShortcutsByCategory(): Record< return grouped; } -export function formatHotkey(keys: string): string { - // Get only the first hotkey if multiple are defined (e.g., "mod+1,mod+2,mod+3") - // But handle edge case where comma is the actual key (e.g., "mod+,") - let hotkey = keys; +function formatKey(key: string): string { + const k = key.trim().toLowerCase(); + if (k === "mod") return isMac ? "⌘" : "Ctrl"; + if (k === "shift") return isMac ? "⇧" : "Shift"; + if (k === "alt") return isMac ? "⌥" : "Alt"; + if (k === "ctrl") return isMac ? "⌃" : "Ctrl"; + if (k === "enter") return isMac ? "↩" : "Enter"; + if (k === "escape" || k === "esc") return "Esc"; + if (k === "up" || k === "arrowup") return "↑"; + if (k === "down" || k === "arrowdown") return "↓"; + if (k === ",") return ","; + if (k === "[") return "["; + if (k === "]") return "]"; + if (k === "tab") return "Tab"; + return k.toUpperCase(); +} + +function extractHotkey(keys: string): string { if (keys.includes(",") && !keys.endsWith(",")) { - hotkey = keys.split(",")[0]; + return keys.split(",")[0]; } + return keys; +} +export function formatHotkey(keys: string): string { + const hotkey = extractHotkey(keys); return hotkey .split("+") - .map((key) => { - const k = key.trim().toLowerCase(); - if (k === "mod") return isMac ? "⌘" : "Ctrl"; - if (k === "shift") return isMac ? "⇧" : "Shift"; - if (k === "alt") return isMac ? "⌥" : "Alt"; - if (k === "ctrl") return isMac ? "⌃" : "Ctrl"; - if (k === "enter") return isMac ? "↩" : "Enter"; - if (k === "escape" || k === "esc") return "Esc"; - if (k === "up" || k === "arrowup") return "↑"; - if (k === "down" || k === "arrowdown") return "↓"; - if (k === ",") return ","; - if (k === "[") return "["; - if (k === "]") return "]"; - if (k === "tab") return "Tab"; - return k.toUpperCase(); - }) + .map(formatKey) .join(isMac ? "" : "+"); } + +export function formatHotkeyParts(keys: string): string[] { + const hotkey = extractHotkey(keys); + return hotkey.split("+").map(formatKey); +} diff --git a/apps/code/src/renderer/features/auth/components/AuthScreen.tsx b/apps/code/src/renderer/features/auth/components/AuthScreen.tsx index c7288e387..dca8408c9 100644 --- a/apps/code/src/renderer/features/auth/components/AuthScreen.tsx +++ b/apps/code/src/renderer/features/auth/components/AuthScreen.tsx @@ -1,284 +1,46 @@ -import { DraggableTitleBar } from "@components/DraggableTitleBar"; -import { ZenHedgehog } from "@components/ZenHedgehog"; -import { - useLoginMutation, - useSignupMutation, -} from "@features/auth/hooks/authMutations"; -import { useAuthUiStateStore } from "@features/auth/stores/authUiStateStore"; -import { Callout, Flex, Spinner, Text, Theme } from "@radix-ui/themes"; -import codeLogo from "@renderer/assets/images/code.svg"; -import logomark from "@renderer/assets/images/logomark.svg"; -import { trpcClient } from "@renderer/trpc/client"; -import type { CloudRegion } from "@shared/types/regions"; -import { REGION_LABELS } from "@shared/types/regions"; -import { RegionSelect } from "./RegionSelect"; - -export const getErrorMessage = (error: unknown) => { - if (!error) { - return null; - } - if (!(error instanceof Error)) { - return "Failed to authenticate"; - } - const message = error.message; - - if (message === "2FA_REQUIRED") { - return null; // 2FA dialog will handle this - } - - if (message.includes("access_denied")) { - return "Authorization cancelled."; - } - - if (message.includes("timed out")) { - return "Authorization timed out. Please try again."; - } - - if (message.includes("SSO login required")) { - return message; - } - - return message; -}; +import { FullScreenLayout } from "@components/FullScreenLayout"; +import { Flex } from "@radix-ui/themes"; +import happyHog from "@renderer/assets/images/hedgehogs/happy-hog.png"; +import { SignInCard } from "./SignInCard"; export function AuthScreen() { - const staleRegion = useAuthUiStateStore((state) => state.staleRegion); - const selectedRegion = useAuthUiStateStore((state) => state.selectedRegion); - const setSelectedRegion = useAuthUiStateStore( - (state) => state.setSelectedRegion, - ); - const authMode = useAuthUiStateStore((state) => state.authMode); - const setAuthMode = useAuthUiStateStore((state) => state.setAuthMode); - const loginMutation = useLoginMutation(); - const signupMutation = useSignupMutation(); - const region: CloudRegion = selectedRegion ?? staleRegion ?? "us"; - - const handleAuth = () => { - if (authMode === "login") { - loginMutation.mutate(region); - } else { - signupMutation.mutate(region); - } - }; - - const handleRegionChange = (value: CloudRegion) => { - setSelectedRegion(value); - loginMutation.reset(); - signupMutation.reset(); - }; - - const handleCancel = async () => { - loginMutation.reset(); - signupMutation.reset(); - await trpcClient.oauth.cancelFlow.mutate(); - }; - - const isPending = loginMutation.isPending || signupMutation.isPending; - const isLoading = isPending; - const error = loginMutation.error || signupMutation.error; - const errorMessage = getErrorMessage(error); - return ( - - - - - {/* Background */} -
- - {/* Right panel — zen hedgehog */} + + - - - - {/* Left side with card */} - - {/* Auth card */} - {/* Logo */} - PostHog - - {/* Error */} - {errorMessage && ( - - {errorMessage} - - )} - - {/* Pending state */} - {isPending && ( - - Waiting for authorization... - - )} - - {/* Primary CTA */} - - - - Redirects to PostHog.com - - - - {/* Region + secondary links */} - - - - - {authMode === "login" ? ( - <> - - Don't have an account?{" "} - - - - ) : ( - <> - - Already have an account?{" "} - - - - )} - + + + + - - {/* Right side - shows background */} -
- + ); } diff --git a/apps/code/src/renderer/features/auth/components/InviteCodeScreen.tsx b/apps/code/src/renderer/features/auth/components/InviteCodeScreen.tsx index 03e90d675..8677111a3 100644 --- a/apps/code/src/renderer/features/auth/components/InviteCodeScreen.tsx +++ b/apps/code/src/renderer/features/auth/components/InviteCodeScreen.tsx @@ -1,12 +1,14 @@ -import { DraggableTitleBar } from "@components/DraggableTitleBar"; +import { FullScreenLayout } from "@components/FullScreenLayout"; +import { OnboardingHogTip } from "@features/onboarding/components/OnboardingHogTip"; +import { SignOut } from "@phosphor-icons/react"; +import { Button, Callout, Flex, Spinner, Text } from "@radix-ui/themes"; +import happyHog from "@renderer/assets/images/hedgehogs/happy-hog.png"; +import { motion } from "framer-motion"; import { useLogoutMutation, useRedeemInviteCodeMutation, -} from "@features/auth/hooks/authMutations"; -import { useAuthUiStateStore } from "@features/auth/stores/authUiStateStore"; -import { Callout, Flex, Spinner, Text, Theme } from "@radix-ui/themes"; -import phWordmark from "@renderer/assets/images/wordmark.svg"; -import zenHedgehog from "@renderer/assets/images/zen.png"; +} from "../hooks/authMutations"; +import { useAuthUiStateStore } from "../stores/authUiStateStore"; export function InviteCodeScreen() { const code = useAuthUiStateStore((state) => state.inviteCode); @@ -14,176 +16,137 @@ export function InviteCodeScreen() { const resetInviteCode = useAuthUiStateStore((state) => state.resetInviteCode); const redeemMutation = useRedeemInviteCodeMutation(); const logoutMutation = useLogoutMutation(); + const errorMessage = redeemMutation.error?.message ?? null; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!code.trim()) return; redeemMutation.mutate(code.trim(), { - onSuccess: () => resetInviteCode(), + onSuccess: () => { + resetInviteCode(); + }, }); }; - const errorMessage = redeemMutation.error?.message ?? null; + const footerRight = ( + + ); return ( - - - - - {/* Background */} -
- - {/* Right panel — zen hedgehog */} + + - - - - {/* Left side with card */} - - {/* Invite code card */} - {/* Logo */} - PostHog - - - Enter your invite code to get started - - - {/* Error */} - {errorMessage && ( - - {errorMessage} - - )} + + + + + Enter your invite code + + + You need an invite code to access PostHog Code. + + + - {/* Form */} -
- - setInviteCode(e.target.value)} - placeholder="Invite code" - disabled={redeemMutation.isPending} - style={{ - width: "100%", - height: "44px", - padding: "0 12px", - border: "1px solid var(--gray-6)", - borderRadius: "10px", - fontSize: "15px", - backgroundColor: "var(--gray-2)", - color: "var(--gray-12)", - outline: "none", - boxSizing: "border-box", - }} - /> - + + + {errorMessage && ( + + {errorMessage} + + )} + setInviteCode(e.target.value)} + placeholder="Invite code" + disabled={redeemMutation.isPending} + style={{ + width: "100%", + height: 44, + padding: "0 14px", + border: "1px solid var(--gray-a3)", + borderRadius: 10, + fontSize: 15, + backgroundColor: "var(--color-panel-solid)", + color: "var(--gray-12)", + outline: "none", + boxSizing: "border-box", + boxShadow: + "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)", + fontFamily: "inherit", + }} + /> + + + +
- - {/* Log out link */} - - +
- - {/* Right side - shows background */} -
- + ); } diff --git a/apps/code/src/renderer/features/auth/components/OAuthControls.tsx b/apps/code/src/renderer/features/auth/components/OAuthControls.tsx new file mode 100644 index 000000000..d7f97eba3 --- /dev/null +++ b/apps/code/src/renderer/features/auth/components/OAuthControls.tsx @@ -0,0 +1,77 @@ +import { useOAuthFlow } from "@features/auth/hooks/useOAuthFlow"; +import { Callout, Flex, Spinner } from "@radix-ui/themes"; +import posthogIcon from "@renderer/assets/images/posthog-icon.svg"; +import { REGION_LABELS } from "@shared/types/regions"; +import { RegionSelect } from "./RegionSelect"; + +export function OAuthControls() { + const { + region, + handleAuth, + handleRegionChange, + handleCancel, + isPending, + errorMessage, + } = useOAuthFlow(); + + return ( + <> + {errorMessage && ( + + {errorMessage} + + )} + + {isPending && ( + + Waiting for authorization... + + )} + + + + + + + + ); +} diff --git a/apps/code/src/renderer/features/auth/components/RegionSelect.tsx b/apps/code/src/renderer/features/auth/components/RegionSelect.tsx index ff3b4b6bb..81aaf3c5f 100644 --- a/apps/code/src/renderer/features/auth/components/RegionSelect.tsx +++ b/apps/code/src/renderer/features/auth/components/RegionSelect.tsx @@ -20,7 +20,7 @@ export function RegionSelect({ if (!expanded) { return ( - + {regionLabel} {" \u00B7 "} @@ -47,7 +47,7 @@ export function RegionSelect({ } return ( - + + + + Sign in / sign up with PostHog + + + {subtitle} + + + + + + ); +} diff --git a/apps/code/src/renderer/features/auth/hooks/useOAuthFlow.ts b/apps/code/src/renderer/features/auth/hooks/useOAuthFlow.ts new file mode 100644 index 000000000..6b5518336 --- /dev/null +++ b/apps/code/src/renderer/features/auth/hooks/useOAuthFlow.ts @@ -0,0 +1,68 @@ +import { useAuthStore } from "@features/auth/stores/authStore"; +import { trpcClient } from "@renderer/trpc/client"; +import type { CloudRegion } from "@shared/types/regions"; +import { useMutation } from "@tanstack/react-query"; +import { useState } from "react"; + +export function getErrorMessage(error: unknown) { + if (!error) { + return null; + } + if (!(error instanceof Error)) { + return "Failed to authenticate"; + } + const message = error.message; + + if (message === "2FA_REQUIRED") { + return null; // 2FA dialog will handle this + } + + if (message.includes("access_denied")) { + return "Authorization cancelled."; + } + + if (message.includes("timed out")) { + return "Authorization timed out. Please try again."; + } + + if (message.includes("SSO login required")) { + return message; + } + + return message; +} + +export function useOAuthFlow() { + const staleRegion = useAuthStore((s) => s.staleCloudRegion); + const [region, setRegion] = useState(staleRegion ?? "us"); + const { loginWithOAuth } = useAuthStore(); + + const loginMutation = useMutation({ + mutationFn: async () => { + await loginWithOAuth(region); + }, + }); + + const handleAuth = () => { + loginMutation.mutate(); + }; + + const handleRegionChange = (value: CloudRegion) => { + setRegion(value); + loginMutation.reset(); + }; + + const handleCancel = async () => { + loginMutation.reset(); + await trpcClient.oauth.cancelFlow.mutate(); + }; + + return { + region, + handleAuth, + handleRegionChange, + handleCancel, + isPending: loginMutation.isPending, + errorMessage: getErrorMessage(loginMutation.error), + }; +} diff --git a/apps/code/src/renderer/features/auth/stores/authStore.test.ts b/apps/code/src/renderer/features/auth/stores/authStore.test.ts index a9e2936fd..a160efb19 100644 --- a/apps/code/src/renderer/features/auth/stores/authStore.test.ts +++ b/apps/code/src/renderer/features/auth/stores/authStore.test.ts @@ -136,7 +136,6 @@ describe("authStore", () => { needsProjectSelection: false, needsScopeReauth: false, hasCodeAccess: null, - hasCompletedOnboarding: false, }); }); diff --git a/apps/code/src/renderer/features/auth/stores/authStore.ts b/apps/code/src/renderer/features/auth/stores/authStore.ts index 456050410..2373c9778 100644 --- a/apps/code/src/renderer/features/auth/stores/authStore.ts +++ b/apps/code/src/renderer/features/auth/stores/authStore.ts @@ -40,7 +40,7 @@ interface AuthStoreState { needsProjectSelection: boolean; needsScopeReauth: boolean; hasCodeAccess: boolean | null; - hasCompletedOnboarding: boolean; + checkCodeAccess: () => Promise; redeemInviteCode: (code: string) => Promise; loginWithOAuth: (region: CloudRegion) => Promise; @@ -202,8 +202,6 @@ export const useAuthStore = create((set) => ({ needsScopeReauth: false, hasCodeAccess: null, - hasCompletedOnboarding: false, - checkCodeAccess: async () => { await syncAuthState(); }, diff --git a/apps/code/src/renderer/features/inbox/components/InboxEmptyStates.tsx b/apps/code/src/renderer/features/inbox/components/InboxEmptyStates.tsx index b3a9fd475..cacb0e52a 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxEmptyStates.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxEmptyStates.tsx @@ -2,8 +2,8 @@ import { AnimatedEllipsis } from "@features/inbox/components/utils/AnimatedEllip import { SOURCE_PRODUCT_META } from "@features/inbox/components/utils/source-product-icons"; import { ArrowDownIcon } from "@phosphor-icons/react"; import { Box, Button, Flex, Text, Tooltip } from "@radix-ui/themes"; -import explorerHog from "@renderer/assets/images/explorer-hog.png"; -import graphsHog from "@renderer/assets/images/graphs-hog.png"; +import explorerHog from "@renderer/assets/images/hedgehogs/explorer-hog.png"; +import graphsHog from "@renderer/assets/images/hedgehogs/graphs-hog.png"; import mailHog from "@renderer/assets/images/mail-hog.png"; // ── Full-width empty states ───────────────────────────────────────────────── diff --git a/apps/code/src/renderer/features/inbox/components/SignalSourceToggles.tsx b/apps/code/src/renderer/features/inbox/components/SignalSourceToggles.tsx index 5854c0b57..95f61cd19 100644 --- a/apps/code/src/renderer/features/inbox/components/SignalSourceToggles.tsx +++ b/apps/code/src/renderer/features/inbox/components/SignalSourceToggles.tsx @@ -14,16 +14,12 @@ import { Box, Button, Flex, - Link, Spinner, Switch, Text, Tooltip, } from "@radix-ui/themes"; -import type { - Evaluation, - SignalSourceConfig, -} from "@renderer/api/posthogClient"; +import type { SignalSourceConfig } from "@renderer/api/posthogClient"; import { memo, useCallback } from "react"; export interface SignalSourceValues { @@ -85,7 +81,7 @@ const SignalSourceToggleCard = memo(function SignalSourceToggleCard({ return ( void; -} - -const EvaluationRow = memo(function EvaluationRow({ - evaluation, - onToggle, -}: EvaluationRowProps) { - const handleChange = useCallback( - (checked: boolean) => onToggle(evaluation.id, checked), - [onToggle, evaluation.id], - ); - - return ( - - - {evaluation.name} - - - - ); -}); - interface EvaluationsSectionProps { - evaluations: Evaluation[]; evaluationsUrl: string; - onToggleEvaluation: (id: string, enabled: boolean) => void; } export const EvaluationsSection = memo(function EvaluationsSection({ - evaluations, evaluationsUrl, - onToggleEvaluation, }: EvaluationsSectionProps) { return ( window.open(evaluationsUrl, "_blank", "noopener")} > - + - + - PostHog LLM Analytics + LLM Analytics Internal - Ongoing evaluation of how your AI features are performing based on - defined criteria + Monitor how your AI features are performing - - - {evaluations.length > 0 ? ( - - {evaluations.map((evaluation) => ( - - ))} - - ) : ( - - No evaluations configured yet. - - )} - - - Manage evaluations in PostHog Cloud - - - + ); @@ -303,9 +241,7 @@ interface SignalSourceTogglesProps { > >; onSetup?: (source: keyof SignalSourceValues) => void; - evaluations?: Evaluation[]; evaluationsUrl?: string; - onToggleEvaluation?: (id: string, enabled: boolean) => void; } export function SignalSourceToggles({ @@ -314,9 +250,7 @@ export function SignalSourceToggles({ disabled, sourceStates, onSetup, - evaluations, evaluationsUrl, - onToggleEvaluation, }: SignalSourceTogglesProps) { const toggleSessionReplay = useCallback( (checked: boolean) => onToggle("session_replay", checked), @@ -347,84 +281,97 @@ export function SignalSourceToggles({ const setupZendesk = useCallback(() => onSetup?.("zendesk"), [onSetup]); return ( - - } - label="PostHog Error Tracking" - description="Surface new issues, reopenings, and volume spikes" - checked={value.error_tracking} - onCheckedChange={toggleErrorTracking} - disabled={disabled} - syncStatus={sourceStates?.error_tracking?.syncStatus} - /> - } - label="PostHog Conversations" - description="Turn support conversations into signals for the inbox" - checked={value.conversations} - onCheckedChange={toggleConversations} - disabled={disabled} - /> - } - label="PostHog Session Replay" - labelSuffix={Alpha} - description="Analyze session recordings and event data for UX issues" - checked={value.session_replay} - onCheckedChange={toggleSessionReplay} - disabled={disabled} - statusSection={ - value.session_replay ? ( - - ) : undefined - } - /> - {evaluations && evaluationsUrl && onToggleEvaluation && ( - - )} - } - label="GitHub Issues" - description="Monitor new issues and updates" - checked={value.github} - onCheckedChange={toggleGithub} - disabled={disabled} - requiresSetup={sourceStates?.github?.requiresSetup} - onSetup={setupGithub} - loading={sourceStates?.github?.loading} - syncStatus={sourceStates?.github?.syncStatus} - /> - } - label="Linear" - description="Monitor new issues and updates" - checked={value.linear} - onCheckedChange={toggleLinear} - disabled={disabled} - requiresSetup={sourceStates?.linear?.requiresSetup} - onSetup={setupLinear} - loading={sourceStates?.linear?.loading} - syncStatus={sourceStates?.linear?.syncStatus} - /> - } - label="Zendesk" - description="Monitor incoming support tickets" - checked={value.zendesk} - onCheckedChange={toggleZendesk} - disabled={disabled} - requiresSetup={sourceStates?.zendesk?.requiresSetup} - onSetup={setupZendesk} - loading={sourceStates?.zendesk?.loading} - syncStatus={sourceStates?.zendesk?.syncStatus} - /> + + {/* PostHog data */} + + + PostHog data + + + } + label="Error Tracking" + description="Surface new issues, reopenings and volume spikes" + checked={value.error_tracking} + onCheckedChange={toggleErrorTracking} + disabled={disabled} + syncStatus={sourceStates?.error_tracking?.syncStatus} + /> + } + label="Conversations" + description="Turn support conversations into signals" + checked={value.conversations} + onCheckedChange={toggleConversations} + disabled={disabled} + /> + } + label="Session Replay" + labelSuffix={Alpha} + description="Analyze recordings for UX issues" + checked={value.session_replay} + onCheckedChange={toggleSessionReplay} + disabled={disabled} + statusSection={ + value.session_replay ? ( + + ) : undefined + } + /> + {evaluationsUrl && ( + + )} + + + + {/* External connections */} + + + External connections + + + } + label="GitHub Issues" + description="Monitor new issues and updates" + checked={value.github} + onCheckedChange={toggleGithub} + disabled={disabled} + requiresSetup={sourceStates?.github?.requiresSetup} + onSetup={setupGithub} + loading={sourceStates?.github?.loading} + syncStatus={sourceStates?.github?.syncStatus} + /> + } + label="Linear" + description="Monitor new issues and updates" + checked={value.linear} + onCheckedChange={toggleLinear} + disabled={disabled} + requiresSetup={sourceStates?.linear?.requiresSetup} + onSetup={setupLinear} + loading={sourceStates?.linear?.loading} + syncStatus={sourceStates?.linear?.syncStatus} + /> + } + label="Zendesk" + description="Monitor incoming support tickets" + checked={value.zendesk} + onCheckedChange={toggleZendesk} + disabled={disabled} + requiresSetup={sourceStates?.zendesk?.requiresSetup} + onSetup={setupZendesk} + loading={sourceStates?.zendesk?.loading} + syncStatus={sourceStates?.zendesk?.syncStatus} + /> + + ); } diff --git a/apps/code/src/renderer/features/onboarding/components/CliInstallStep.tsx b/apps/code/src/renderer/features/onboarding/components/CliInstallStep.tsx new file mode 100644 index 000000000..d51cb2acd --- /dev/null +++ b/apps/code/src/renderer/features/onboarding/components/CliInstallStep.tsx @@ -0,0 +1,380 @@ +import { + ArrowLeft, + ArrowRight, + ArrowSquareOut, + ArrowsClockwise, + CheckCircle, + CircleNotch, + GitBranch, + GithubLogo, + Terminal, + Warning, +} from "@phosphor-icons/react"; +import { Box, Button, Code, Flex, Text } from "@radix-ui/themes"; +import builderHog from "@renderer/assets/images/hedgehogs/builder-hog-03.png"; +import { trpcClient, useTRPC } from "@renderer/trpc/client"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { EXTERNAL_LINKS } from "@utils/links"; +import { motion } from "framer-motion"; +import { useCallback, useState } from "react"; +import { OnboardingHogTip } from "./OnboardingHogTip"; +import { StepActions } from "./StepActions"; + +interface CliInstallStepProps { + onNext: () => void; + onBack: () => void; +} + +export function CliInstallStep({ onNext, onBack }: CliInstallStepProps) { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const [isCheckingGit, setIsCheckingGit] = useState(false); + const [isCheckingGh, setIsCheckingGh] = useState(false); + + const { data: gitStatus, isLoading: isLoadingGit } = useQuery( + trpc.git.getGitStatus.queryOptions(undefined, { staleTime: 30_000 }), + ); + const { data: ghStatus, isLoading: isLoadingGh } = useQuery( + trpc.git.getGhStatus.queryOptions(undefined, { staleTime: 30_000 }), + ); + + const gitInstalled = gitStatus?.installed ?? false; + const ghInstalled = ghStatus?.installed ?? false; + const ghAuthenticated = ghStatus?.authenticated ?? false; + const allReady = gitInstalled && ghInstalled && ghAuthenticated; + + const handleCheckGit = useCallback(async () => { + setIsCheckingGit(true); + await queryClient.invalidateQueries(trpc.git.getGitStatus.queryFilter()); + setIsCheckingGit(false); + }, [queryClient, trpc]); + + const handleCheckGh = useCallback(async () => { + setIsCheckingGh(true); + await queryClient.invalidateQueries(trpc.git.getGhStatus.queryFilter()); + setIsCheckingGh(false); + }, [queryClient, trpc]); + + return ( + + + + + + + + + Install required tools + + + These CLI tools are needed for code management and GitHub + workflows. + + + + + {/* Git box */} + + + + + + + + Git + + + {isLoadingGit && ( + + )} + {!isLoadingGit && gitInstalled && ( + + + + Installed + {gitStatus?.version + ? ` (${gitStatus.version})` + : ""} + + + )} + + {!isLoadingGit && !gitInstalled && ( + + + Install with Homebrew or Xcode Command Line Tools: + + + + + + brew install git + + + + + + xcode-select --install + + + + + + + + + )} + + + + + {/* GitHub CLI box */} + + + + + + + + GitHub CLI + + + {isLoadingGh && ( + + )} + {!isLoadingGh && ghInstalled && ghAuthenticated && ( + + + + {ghStatus?.username + ? `Logged in as ${ghStatus.username}` + : "Authenticated"} + + + )} + {!isLoadingGh && ghInstalled && !ghAuthenticated && ( + + + + Not logged in + + + )} + + {!isLoadingGh && !ghInstalled && ( + + + Install with Homebrew: + + + + + brew install gh + + + + + + + + )} + {!isLoadingGh && ghInstalled && !ghAuthenticated && ( + + + Run this in your terminal to log in: + + + + + gh auth login + + + + + )} + + + + + + + + + + + + {allReady ? ( + + ) : ( + + )} + + + + ); +} diff --git a/apps/code/src/renderer/features/onboarding/components/FeatureListItem.css b/apps/code/src/renderer/features/onboarding/components/FeatureListItem.css new file mode 100644 index 000000000..a5b389c20 --- /dev/null +++ b/apps/code/src/renderer/features/onboarding/components/FeatureListItem.css @@ -0,0 +1,16 @@ +.feature-list-item { + border-left: 2px solid var(--gray-4); + transition: + border-color 0.2s ease, + transform 0.2s ease; +} + +.feature-list-item:hover, +.feature-list-item--active { + border-left-color: var(--accent-9); + transform: translateX(6px); +} + +.feature-list-item .feature-list-item__description { + display: block; +} diff --git a/apps/code/src/renderer/features/onboarding/components/FeatureListItem.tsx b/apps/code/src/renderer/features/onboarding/components/FeatureListItem.tsx index 6620fe177..6063cf266 100644 --- a/apps/code/src/renderer/features/onboarding/components/FeatureListItem.tsx +++ b/apps/code/src/renderer/features/onboarding/components/FeatureListItem.tsx @@ -1,56 +1,77 @@ import { Flex, Text } from "@radix-ui/themes"; +import { motion } from "framer-motion"; import type { ReactNode } from "react"; +import "./FeatureListItem.css"; interface FeatureListItemProps { icon: ReactNode; title: string; description: string; + active?: boolean; + index?: number; + onMouseEnter?: () => void; + onMouseLeave?: () => void; } export function FeatureListItem({ icon, title, description, + active = false, + index = 0, + onMouseEnter, + onMouseLeave, }: FeatureListItemProps) { return ( - - {icon} - - - - {title} - - - {description} - + {icon} + + + + {title} + + + {description} + + - + ); } diff --git a/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx b/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx index 99a59d6c1..13f31ae65 100644 --- a/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx @@ -1,16 +1,23 @@ -import { useAuthenticatedClient } from "@features/auth/hooks/authClient"; +import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient"; import { useSelectProjectMutation } from "@features/auth/hooks/authMutations"; import { useAuthStateValue } from "@features/auth/hooks/authQueries"; +import { FolderPicker } from "@features/folder-picker/components/FolderPicker"; +import { useIntegrationSelectors } from "@features/integrations/stores/integrationStore"; import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore"; +import { useRepositoryIntegration } from "@hooks/useIntegrations"; import { ArrowLeft, ArrowRight, ArrowSquareOut, + ArrowsClockwise, CheckCircle, + CircleNotch, + FolderOpen, + GearSix, GitBranch, } from "@phosphor-icons/react"; import { Box, Button, Callout, Flex, Skeleton, Text } from "@radix-ui/themes"; -import codeLogo from "@renderer/assets/images/code.svg"; +import builderHog from "@renderer/assets/images/hedgehogs/builder-hog-03.png"; import { trpcClient } from "@renderer/trpc/client"; import { IS_DEV } from "@shared/constants/environment"; import { useQueryClient } from "@tanstack/react-query"; @@ -18,28 +25,38 @@ import { AnimatePresence, motion } from "framer-motion"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { useGitHubIntegrationCallback } from "../../integrations/hooks/useGitHubIntegrationCallback"; +import type { DetectedRepo } from "../hooks/useOnboardingFlow"; import { useProjectsWithIntegrations } from "../hooks/useProjectsWithIntegrations"; -import { ProjectSelect } from "./ProjectSelect"; +import { OnboardingHogTip } from "./OnboardingHogTip"; +import { StepActions } from "./StepActions"; const POLL_INTERVAL_MS = 3_000; -const POLL_TIMEOUT_MS = 300_000; // 5 minutes +const POLL_TIMEOUT_MS = 300_000; interface GitIntegrationStepProps { onNext: () => void; onBack: () => void; + selectedDirectory: string; + detectedRepo: DetectedRepo | null; + isDetectingRepo: boolean; + onDirectoryChange: (path: string) => void; } export function GitIntegrationStep({ onNext, onBack, + selectedDirectory, + detectedRepo, + isDetectingRepo, + onDirectoryChange, }: GitIntegrationStepProps) { const cloudRegion = useAuthStateValue((state) => state.cloudRegion); const currentProjectId = useAuthStateValue((state) => state.projectId); - const client = useAuthenticatedClient(); + const client = useOptionalAuthenticatedClient(); const selectProjectMutation = useSelectProjectMutation(); const queryClient = useQueryClient(); - const { projects, projectsWithGithub, isLoading, isFetching } = + const { projects, projectsWithGithub, isLoading } = useProjectsWithIntegrations(); const isConnecting = useOnboardingStore((state) => state.isConnectingGithub); @@ -56,10 +73,6 @@ export function GitIntegrationStep({ const pollTimeoutRef = useRef | null>(null); const [timedOut, setTimedOut] = useState(false); - // Determine which project to show: - // 1. If user manually selected one, use that - // 2. Current project from auth (matches user's active PostHog project) - // 3. Fall back to first available const selectedProjectId = useMemo(() => { if (manuallySelectedProjectId !== null) { return manuallySelectedProjectId; @@ -73,24 +86,30 @@ export function GitIntegrationStep({ ); const hasGitIntegration = selectedProject?.hasGithubIntegration ?? false; + const { repositories, isLoadingRepos } = useRepositoryIntegration(); + const { githubIntegrations } = useIntegrationSelectors(); + const githubIntegration = githubIntegrations[0] ?? null; - const connectedAccountName = useMemo(() => { - const github = selectedProject?.integrations.find( - (i) => i.kind === "github", - ); - const name = github?.config?.account?.name; - return typeof name === "string" && name.length > 0 ? name : null; - }, [selectedProject]); - - // Surface a banner when the selected project has no integration but some - // other project does — a common onboarding edge case where the GitHub App is - // already installed for a different PostHog project/org. const alternativeConnectedProject = useMemo(() => { if (hasGitIntegration) return null; if (!projectsWithGithub.length) return null; return projectsWithGithub.find((p) => p.id !== selectedProjectId) ?? null; }, [hasGitIntegration, projectsWithGithub, selectedProjectId]); + const repoSummary = useMemo(() => { + if (repositories.length === 0) return null; + const names = repositories.map((r) => r.split("/").pop() ?? r); + if (names.length <= 2) return names.join(" and "); + return `${names[0]}, ${names[1]} and ${names.length - 2} more`; + }, [repositories]); + + const repoMatchesGitHub = useMemo(() => { + if (!detectedRepo || repositories.length === 0) return false; + return repositories.some( + (r) => r.toLowerCase() === detectedRepo.fullName.toLowerCase(), + ); + }, [detectedRepo, repositories]); + const stopPolling = useCallback(() => { if (pollTimerRef.current) { clearInterval(pollTimerRef.current); @@ -102,7 +121,6 @@ export function GitIntegrationStep({ } }, []); - // Stop polling when integration is detected useEffect(() => { if (hasGitIntegration && isConnecting) { stopPolling(); @@ -111,7 +129,6 @@ export function GitIntegrationStep({ } }, [hasGitIntegration, isConnecting, setConnectingGithub, stopPolling]); - // Cleanup on unmount useEffect(() => stopPolling, [stopPolling]); const invalidateProject = useCallback( @@ -179,12 +196,7 @@ export function GitIntegrationStep({ } }; - const handleRefresh = () => { - invalidateProject(selectedProjectId); - }; - const handleContinue = () => { - // Persist the selected project if it's different from current if (selectedProjectId && selectedProjectId !== currentProjectId) { selectProjectMutation.mutate(selectedProjectId); } @@ -195,369 +207,340 @@ export function GitIntegrationStep({ - PostHog - - - - + {/* Header + content */} + + - Connect your Git repository - - - PostHog Code needs access to your GitHub repositories to enable - cloud runs and PR creation. - - - {selectedProject && ( - + - {selectedProject.organization.name} + Give your agents access to code + + + Point to a local codebase and optionally connect GitHub. - ({ - id: p.id, - name: p.name, - }))} - onProjectChange={setSelectedProjectId} - disabled={isLoading} - /> - - )} - - - {alternativeConnectedProject && selectedProject && ( - - - GitHub is already connected on{" "} - {alternativeConnectedProject.name}{" "} - ({alternativeConnectedProject.organization.name}). Switch to - that project, or click Connect to install a new integration on{" "} - {selectedProject.name}. - - - - - )} + - {/* Consistent status box - same height regardless of connection state */} - - - - {isLoading ? ( - - - - ) : hasGitIntegration ? ( - - - - ) : ( - + + GitHub is already connected on{" "} + + {alternativeConnectedProject.name} + {" "} + ({alternativeConnectedProject.organization.name}). Switch to + that project, or click Connect to install a new integration + on {selectedProject.name}. + + + - - {timedOut - ? "We didn't hear back from GitHub. If the browser tab was closed, click Connect again." - : isConnecting - ? "Waiting for GitHub\u2026 You'll return here automatically once the install completes." - : "Opens GitHub to authorize the PostHog app"} + + + Select the local folder for your project so we can + analyze it. - - - ) : ( - - - - )} - - - - + + + + {isDetectingRepo && ( + + + + + Detecting repository... + + + + )} + {!isDetectingRepo && + selectedDirectory && + detectedRepo && ( + + + + + {repoMatchesGitHub + ? `Linked to ${detectedRepo.fullName} on GitHub` + : `Detected ${detectedRepo.fullName}`} + + + + )} + {!isDetectingRepo && + selectedDirectory && + !detectedRepo && ( + + + No git remote detected -- you can still continue. + + + )} + + + + - - {!isLoading && ( + {/* GitHub integration */} - - - {hasGitIntegration ? ( - - ) : ( - - )} - + + + + + + Connect GitHub + + + {isLoading ? ( + + ) : hasGitIntegration ? ( + + + + Connected + + + ) : null} + + {hasGitIntegration ? ( + + + {isLoadingRepos + ? "Loading repositories..." + : repoSummary + ? `Access to ${repoSummary}` + : "No repositories found. Check your GitHub app settings."} + + + + + + + ) : !isLoading ? ( + + + {timedOut + ? "We didn't hear back from GitHub. If the browser tab was closed, click Connect again." + : isConnecting + ? "Waiting for GitHub... You'll return here automatically once the install completes." + : "Optional. Unlocks cloud agents and pull request workflows."} + + + + ) : null} + + - )} - + + + {/* Hog tip */} + + + + + + + ); diff --git a/apps/code/src/renderer/features/onboarding/components/InviteCodeStep.tsx b/apps/code/src/renderer/features/onboarding/components/InviteCodeStep.tsx new file mode 100644 index 000000000..6b61f5f47 --- /dev/null +++ b/apps/code/src/renderer/features/onboarding/components/InviteCodeStep.tsx @@ -0,0 +1,148 @@ +import { useRedeemInviteCodeMutation } from "@features/auth/hooks/authMutations"; +import { useAuthUiStateStore } from "@features/auth/stores/authUiStateStore"; +import { ArrowLeft, ArrowRight } from "@phosphor-icons/react"; +import { Button, Callout, Flex, Spinner, Text } from "@radix-ui/themes"; +import happyHog from "@renderer/assets/images/hedgehogs/happy-hog.png"; +import { motion } from "framer-motion"; +import { OnboardingHogTip } from "./OnboardingHogTip"; +import { StepActions } from "./StepActions"; + +interface InviteCodeStepProps { + onNext: () => void; + onBack: () => void; +} + +export function InviteCodeStep({ onNext, onBack }: InviteCodeStepProps) { + const code = useAuthUiStateStore((state) => state.inviteCode); + const setInviteCode = useAuthUiStateStore((state) => state.setInviteCode); + const resetInviteCode = useAuthUiStateStore((state) => state.resetInviteCode); + const redeemMutation = useRedeemInviteCodeMutation(); + const errorMessage = redeemMutation.error?.message ?? null; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!code.trim()) return; + redeemMutation.mutate(code.trim(), { + onSuccess: () => { + resetInviteCode(); + onNext(); + }, + }); + }; + + return ( + + + + + + + + + Enter your invite code + + + You need an invite code to access PostHog Code. + + + + + +
+ + {errorMessage && ( + + {errorMessage} + + )} + setInviteCode(e.target.value)} + placeholder="Invite code" + disabled={redeemMutation.isPending} + style={{ + width: "100%", + height: 44, + padding: "0 14px", + border: "1px solid var(--gray-a3)", + borderRadius: 10, + fontSize: 15, + backgroundColor: "var(--color-panel-solid)", + color: "var(--gray-12)", + outline: "none", + boxSizing: "border-box", + boxShadow: + "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)", + fontFamily: "inherit", + }} + /> + + +
+
+
+ + +
+
+ + + + +
+
+ ); +} diff --git a/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx b/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx index 0dde1750a..2cb2ef939 100644 --- a/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx +++ b/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx @@ -1,173 +1,194 @@ -import { DraggableTitleBar } from "@components/DraggableTitleBar"; -import { ZenHedgehog } from "@components/ZenHedgehog"; +import { FullScreenLayout } from "@components/FullScreenLayout"; import { useLogoutMutation } from "@features/auth/hooks/authMutations"; +import { useAuthStateValue } from "@features/auth/hooks/authQueries"; import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore"; -import { SignOut } from "@phosphor-icons/react"; -import { Button, Flex, Theme } from "@radix-ui/themes"; +import { ArrowRight, SignOut } from "@phosphor-icons/react"; +import { Button, Flex } from "@radix-ui/themes"; +import { IS_DEV } from "@shared/constants/environment"; import { AnimatePresence, LayoutGroup, motion } from "framer-motion"; +import { useHotkeys } from "react-hotkeys-hook"; import { useOnboardingFlow } from "../hooks/useOnboardingFlow"; +import { usePrefetchSignalData } from "../hooks/usePrefetchSignalData"; +import { CliInstallStep } from "./CliInstallStep"; import { GitIntegrationStep } from "./GitIntegrationStep"; -import { OrgStep } from "./OrgStep"; +import { InviteCodeStep } from "./InviteCodeStep"; +import { ProjectSelectStep } from "./ProjectSelectStep"; import { SignalsStep } from "./SignalsStep"; import { StepIndicator } from "./StepIndicator"; -import { WelcomeStep } from "./WelcomeStep"; +import { WelcomeScreen } from "./WelcomeScreen"; + +const stepVariants = { + enter: (dir: number) => ({ opacity: 0, x: dir * 20 }), + center: { opacity: 1, x: 0 }, + exit: (dir: number) => ({ opacity: 0, x: dir * -20 }), +}; export function OnboardingFlow() { - const { currentStep, activeSteps, next, back } = useOnboardingFlow(); + const { + currentStep, + activeSteps, + direction, + next, + back, + selectedDirectory, + detectedRepo, + isDetectingRepo, + handleDirectoryChange, + } = useOnboardingFlow(); const completeOnboarding = useOnboardingStore( (state) => state.completeOnboarding, ); + const resetOnboarding = useOnboardingStore((state) => state.resetOnboarding); const logoutMutation = useLogoutMutation(); + const isAuthenticated = useAuthStateValue( + (state) => state.status === "authenticated", + ); + usePrefetchSignalData(); + + useHotkeys("right", next, { enableOnFormTags: false }, [next]); + useHotkeys("left", back, { enableOnFormTags: false }, [back]); const handleComplete = () => { completeOnboarding(); }; - return ( - - - + {isAuthenticated && ( + + )} + {IS_DEV && ( + + )} + + ); - {/* Right panel — zen hedgehog */} - - - + return ( + + + + {currentStep === "welcome" && ( + + + + )} - {/* Content */} - - - - {currentStep === "welcome" && ( - - - - )} + + + )} - {currentStep === "org" && ( - - - - )} + {currentStep === "invite-code" && ( + + + + )} - {currentStep === "git-integration" && ( - - - - )} + {currentStep === "github" && ( + + + + )} - {currentStep === "signals" && ( - - - - )} - - + {currentStep === "install-cli" && ( + + + + )} - - - - - - - + + + )} + + + - + ); } diff --git a/apps/code/src/renderer/features/onboarding/components/OnboardingHogTip.tsx b/apps/code/src/renderer/features/onboarding/components/OnboardingHogTip.tsx new file mode 100644 index 000000000..db10533ca --- /dev/null +++ b/apps/code/src/renderer/features/onboarding/components/OnboardingHogTip.tsx @@ -0,0 +1,123 @@ +import { Flex, Text } from "@radix-ui/themes"; +import { motion, useAnimationControls } from "framer-motion"; +import { useCallback, useEffect, useRef } from "react"; + +interface OnboardingHogTipProps { + hogSrc: string; + message: string; + delay?: number; +} + +const talkingAnimation = { + rotate: [0, -3, 3, -2, 2, 0], + y: [0, -2, 0, -1, 0], + transition: { + duration: 0.4, + repeat: Infinity, + repeatDelay: 0.1, + }, +}; + +export function OnboardingHogTip({ + hogSrc, + message, + delay = 0.1, +}: OnboardingHogTipProps) { + const controls = useAnimationControls(); + + const isHovering = useRef(false); + + useEffect(() => { + const startDelay = (delay + 0.3) * 1000; + const startTimer = setTimeout(() => { + controls.start(talkingAnimation); + }, startDelay); + const stopTimer = setTimeout(() => { + if (!isHovering.current) { + controls.stop(); + controls.set({ rotate: 0, y: 0 }); + } + }, startDelay + 5000); + return () => { + clearTimeout(startTimer); + clearTimeout(stopTimer); + }; + }, [controls, delay]); + + const handleMouseEnter = useCallback(() => { + isHovering.current = true; + controls.start(talkingAnimation); + }, [controls]); + + const handleMouseLeave = useCallback(() => { + isHovering.current = false; + controls.stop(); + controls.set({ rotate: 0, y: 0 }); + }, [controls]); + + return ( + + + +
+ {/* Border tail */} +
+ {/* Fill tail */} +
+ + {message} + +
+ + + ); +} diff --git a/apps/code/src/renderer/features/onboarding/components/OrgStep.tsx b/apps/code/src/renderer/features/onboarding/components/OrgStep.tsx deleted file mode 100644 index 6d581e483..000000000 --- a/apps/code/src/renderer/features/onboarding/components/OrgStep.tsx +++ /dev/null @@ -1,339 +0,0 @@ -import { useAuthenticatedClient } from "@features/auth/hooks/authClient"; -import { - authKeys, - useAuthStateValue, - useCurrentUser, -} from "@features/auth/hooks/authQueries"; -import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore"; -import { useProjects } from "@features/projects/hooks/useProjects"; -import { useOrganizations } from "@hooks/useOrganizations"; -import { ArrowLeft, ArrowRight, CheckCircle } from "@phosphor-icons/react"; -import { - Box, - Button, - Callout, - Flex, - Select, - Skeleton, - Text, -} from "@radix-ui/themes"; -import codeLogo from "@renderer/assets/images/code.svg"; -import { trpcClient } from "@renderer/trpc/client"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { logger } from "@utils/logger"; -import { AnimatePresence, motion } from "framer-motion"; -import { useMemo } from "react"; - -const log = logger.scope("org-step"); - -interface OrgStepProps { - onNext: () => void; - onBack: () => void; -} - -export function OrgStep({ onNext, onBack }: OrgStepProps) { - const selectedOrgId = useOnboardingStore((state) => state.selectedOrgId); - const selectOrg = useOnboardingStore((state) => state.selectOrg); - const manuallySelectedProjectId = useOnboardingStore( - (state) => state.selectedProjectId, - ); - const setSelectedProjectId = useOnboardingStore( - (state) => state.selectProjectId, - ); - const client = useAuthenticatedClient(); - const { data: currentUser } = useCurrentUser({ client }); - const currentProjectId = useAuthStateValue((state) => state.projectId); - const queryClient = useQueryClient(); - - const switchOrganizationMutation = useMutation({ - mutationFn: async (orgId: string) => { - await client.switchOrganization(orgId); - await queryClient.invalidateQueries({ - queryKey: authKeys.currentUsers(), - }); - }, - onError: (err) => { - log.error("Failed to switch organization", err); - }, - }); - - const { orgs, effectiveSelectedOrgId, isLoading, error } = useOrganizations(); - - const currentUserOrgId = currentUser?.organization?.id; - const hasOrgChanged = effectiveSelectedOrgId !== currentUserOrgId; - - const { projects, isLoading: projectsLoading } = useProjects(); - - const selectedProjectId = useMemo(() => { - if (manuallySelectedProjectId !== null) return manuallySelectedProjectId; - return currentProjectId ?? projects[0]?.id ?? null; - }, [manuallySelectedProjectId, currentProjectId, projects]); - - const handleContinue = async () => { - if (!effectiveSelectedOrgId) return; - - if (effectiveSelectedOrgId !== selectedOrgId) { - selectOrg(effectiveSelectedOrgId); - } - - if (client && hasOrgChanged) { - try { - await switchOrganizationMutation.mutateAsync(effectiveSelectedOrgId); - } catch { - // Error handled by onError callback - } - } - - if ( - !hasOrgChanged && - selectedProjectId && - selectedProjectId !== currentProjectId - ) { - await trpcClient.auth.selectProject.mutate({ - projectId: selectedProjectId, - }); - } - - onNext(); - }; - - const handleSelectOrg = (orgId: string) => { - selectOrg(orgId); - setSelectedProjectId(null); - }; - - return ( - - - - PostHog - - Choose your organization - - - Select which PostHog organization and project to use with PostHog - Code. - - - - {error && ( - - - Failed to load organizations. Please try again later. - - - )} - - - - - Organization - - - {isLoading ? ( - - - - - - - - - - - ) : ( - - - {orgs.map((org) => ( - handleSelectOrg(org.id)} - /> - ))} - - - )} - - - - {!isLoading && !hasOrgChanged && projects.length > 0 && ( - - - Project - - setSelectedProjectId(Number(value))} - size="2" - disabled={projectsLoading} - > - - - {projects.map((project) => ( - - {project.name} - - ))} - - - - )} - - - - - - - - - ); -} - -interface OrgCardProps { - name: string; - isSelected: boolean; - onSelect: () => void; -} - -function OrgCard({ name, isSelected, onSelect }: OrgCardProps) { - return ( - - - - {name} - - - - - {isSelected && ( - - )} - - - ); -} diff --git a/apps/code/src/renderer/features/onboarding/components/ProjectSelect.css b/apps/code/src/renderer/features/onboarding/components/ProjectSelect.css index 73bc50ed5..3a6910188 100644 --- a/apps/code/src/renderer/features/onboarding/components/ProjectSelect.css +++ b/apps/code/src/renderer/features/onboarding/components/ProjectSelect.css @@ -1,6 +1,5 @@ .project-select-popover [cmdk-root] { - width: 320px; - min-width: 320px; + width: 100%; background: var(--color-panel-solid); border-radius: var(--radius-3); border: 1px solid var(--gray-6); diff --git a/apps/code/src/renderer/features/onboarding/components/ProjectSelect.tsx b/apps/code/src/renderer/features/onboarding/components/ProjectSelect.tsx index 82c4eb13a..26687d6f7 100644 --- a/apps/code/src/renderer/features/onboarding/components/ProjectSelect.tsx +++ b/apps/code/src/renderer/features/onboarding/components/ProjectSelect.tsx @@ -10,6 +10,7 @@ interface ProjectSelectProps { projects: Array<{ id: number; name: string }>; onProjectChange: (projectId: number) => void; disabled?: boolean; + size?: "1" | "2"; } export function ProjectSelect({ @@ -18,6 +19,7 @@ export function ProjectSelect({ projects, onProjectChange, disabled = false, + size = "2", }: ProjectSelectProps) { const [open, setOpen] = useState(false); const currentProject = projects.find((p) => p.id === projectId); @@ -28,14 +30,14 @@ export function ProjectSelect({ if (projects.length <= 1) { return ( - + {projectName} ); } return ( - + {projectName} {" · "} diff --git a/apps/code/src/renderer/features/onboarding/components/ProjectSelectStep.tsx b/apps/code/src/renderer/features/onboarding/components/ProjectSelectStep.tsx new file mode 100644 index 000000000..f79e0eb62 --- /dev/null +++ b/apps/code/src/renderer/features/onboarding/components/ProjectSelectStep.tsx @@ -0,0 +1,445 @@ +import { SignInCard } from "@features/auth/components/SignInCard"; +import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient"; +import { useSelectProjectMutation } from "@features/auth/hooks/authMutations"; +import { + authKeys, + useAuthStateValue, + useCurrentUser, +} from "@features/auth/hooks/authQueries"; +import { Command } from "@features/command/components/Command"; +import { useProjects } from "@features/projects/hooks/useProjects"; +import { + ArrowLeft, + ArrowRight, + CaretDown, + Check, + CheckCircle, +} from "@phosphor-icons/react"; +import { Box, Button, Flex, Popover, Spinner, Text } from "@radix-ui/themes"; +import happyHog from "@renderer/assets/images/hedgehogs/happy-hog.png"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { logger } from "@utils/logger"; +import { AnimatePresence, motion } from "framer-motion"; +import { useEffect, useMemo, useState } from "react"; +import { OnboardingHogTip } from "./OnboardingHogTip"; +import { StepActions } from "./StepActions"; + +import "./ProjectSelect.css"; + +const log = logger.scope("project-select-step"); + +interface ProjectSelectStepProps { + onNext: () => void; + onBack: () => void; +} + +export function ProjectSelectStep({ onNext, onBack }: ProjectSelectStepProps) { + const isAuthenticated = + useAuthStateValue((state) => state.status) === "authenticated"; + const selectProjectMutation = useSelectProjectMutation(); + const currentProjectId = useAuthStateValue((state) => state.projectId); + const { projects, currentProject, currentUser, isLoading } = useProjects(); + const [projectOpen, setProjectOpen] = useState(false); + const [orgOpen, setOrgOpen] = useState(false); + const [isSwitchingOrg, setIsSwitchingOrg] = useState(false); + + const client = useOptionalAuthenticatedClient(); + const queryClient = useQueryClient(); + const { data: fullUser } = useCurrentUser({ client }); + + const organizations = useMemo(() => { + if (!fullUser?.organizations) return []; + return fullUser.organizations as Array<{ + id: string; + name: string; + slug: string; + }>; + }, [fullUser]); + + const currentOrg = fullUser?.organization as + | { id: string; name: string } + | undefined; + const hasMultipleOrgs = organizations.length > 1; + + const switchOrgMutation = useMutation({ + mutationFn: async (orgId: string) => { + if (!client) return; + await client.switchOrganization(orgId); + await queryClient.invalidateQueries({ + queryKey: authKeys.currentUsers(), + }); + }, + onMutate: () => { + setIsSwitchingOrg(true); + }, + onError: (err) => { + setIsSwitchingOrg(false); + log.error("Failed to switch organization", err); + }, + }); + + useEffect(() => { + if (isSwitchingOrg && !switchOrgMutation.isPending && !isLoading) { + setIsSwitchingOrg(false); + } + }, [isSwitchingOrg, switchOrgMutation.isPending, isLoading]); + + return ( + + + + + {/* Header + form */} + + {/* Section 1: Sign in */} + + + {isAuthenticated ? ( + + + + Pick your PostHog home base + + + Choose the organization and project you want to work + in. + + + + ) : ( + + + + )} + + + + {/* Sections 2+3: Org & project selectors (authenticated only) */} + {isAuthenticated && (isLoading || isSwitchingOrg) && ( + + + + )} + + {isAuthenticated && !isSwitchingOrg && hasMultipleOrgs && ( + + + + Organization + + + + + + + + + + + + No organizations found. + + {[...organizations] + .sort((a, b) => a.name.localeCompare(b.name)) + .map((org) => ( + { + if (org.id !== currentOrg?.id) { + switchOrgMutation.mutate(org.id); + } + setOrgOpen(false); + }} + > + + + {org.name} + + {org.id === currentOrg?.id && ( + + )} + + + ))} + + + + + + + )} + + {/* Section 3: Project selector (only when authenticated, not switching, and loaded) */} + {isAuthenticated && !isSwitchingOrg && !isLoading && ( + + + + Project + + + + + + + + + + No projects found. + {[...projects] + .sort((a, b) => a.name.localeCompare(b.name)) + .map((project) => ( + { + selectProjectMutation.mutate(project.id); + setProjectOpen(false); + }} + > + + + {project.name} + + {project.id === currentProjectId && ( + + )} + + + ))} + + + + + + + )} + + {/* Signed in confirmation */} + {isAuthenticated && !isLoading && !isSwitchingOrg && ( + + + + Signed in as {currentUser?.email} + + + )} + + + {/* Hog tip */} + {isAuthenticated && !isLoading && !isSwitchingOrg && ( + + )} + + + + + + {isAuthenticated && !isLoading && ( + + )} + + + + ); +} diff --git a/apps/code/src/renderer/features/onboarding/components/SignalsStep.tsx b/apps/code/src/renderer/features/onboarding/components/SignalsStep.tsx index 561ab992b..220b209e3 100644 --- a/apps/code/src/renderer/features/onboarding/components/SignalsStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/SignalsStep.tsx @@ -4,9 +4,11 @@ import { useSignalSourceManager } from "@features/inbox/hooks/useSignalSourceMan import { useMeQuery } from "@hooks/useMeQuery"; import { ArrowLeft, ArrowRight } from "@phosphor-icons/react"; import { Button, Flex, Text } from "@radix-ui/themes"; -import codeLogo from "@renderer/assets/images/code.svg"; +import detectiveHog from "@renderer/assets/images/hedgehogs/detective-hog.png"; import { useQueryClient } from "@tanstack/react-query"; import { motion } from "framer-motion"; +import { OnboardingHogTip } from "./OnboardingHogTip"; +import { StepActions } from "./StepActions"; interface SignalsStepProps { onNext: () => void; @@ -24,9 +26,7 @@ export function SignalsStep({ onNext, onBack }: SignalsStepProps) { handleSetup, handleSetupComplete, handleSetupCancel, - evaluations, evaluationsUrl, - handleToggleEvaluation, } = useSignalSourceManager(); const { data: me } = useMeQuery(); const isStaff = me?.is_staff ?? false; @@ -51,120 +51,123 @@ export function SignalsStep({ onNext, onBack }: SignalsStepProps) { - PostHog - - - - + {/* Header + content */} + + - Enable Inbox - - - Inbox automatically analyzes your product data and prioritizes - actionable tasks. Choose which sources to enable for this - project. - + + + Set up your Signals Inbox + + + Choose which sources to monitor for this project. Signals + will analyze activity and prioritize what needs attention. + + + + + + {setupSource ? ( + void handleSetupComplete()} + onCancel={handleSetupCancel} + /> + ) : ( + + void handleToggle(source, enabled) + } + disabled={isLoading} + sourceStates={sourceStates} + onSetup={handleSetup} + evaluationsUrl={isStaff ? evaluationsUrl : undefined} + /> + )} + - {setupSource ? ( - void handleSetupComplete()} - onCancel={handleSetupCancel} - /> - ) : ( - - void handleToggle(source, enabled) - } - disabled={isLoading} - sourceStates={sourceStates} - onSetup={handleSetup} - evaluations={isStaff ? evaluations : undefined} - evaluationsUrl={isStaff ? evaluationsUrl : undefined} - onToggleEvaluation={ - isStaff - ? (id, enabled) => void handleToggleEvaluation(id, enabled) - : undefined - } - /> - )} + {/* Hog tip */} + + - + + {anyEnabled ? ( + - {anyEnabled ? ( - - ) : ( - - )} - - - + Continue + + + ) : ( + + )} + ); diff --git a/apps/code/src/renderer/features/onboarding/components/StepActions.tsx b/apps/code/src/renderer/features/onboarding/components/StepActions.tsx new file mode 100644 index 000000000..0cd6dacec --- /dev/null +++ b/apps/code/src/renderer/features/onboarding/components/StepActions.tsx @@ -0,0 +1,23 @@ +import { Flex } from "@radix-ui/themes"; +import { motion } from "framer-motion"; +import type { ReactNode } from "react"; + +interface StepActionsProps { + children: ReactNode; + delay?: number; +} + +export function StepActions({ children, delay = 0.15 }: StepActionsProps) { + return ( + + + {children} + + + ); +} diff --git a/apps/code/src/renderer/features/onboarding/components/StepIndicator.tsx b/apps/code/src/renderer/features/onboarding/components/StepIndicator.tsx index 17d1d45ac..7cf01b8db 100644 --- a/apps/code/src/renderer/features/onboarding/components/StepIndicator.tsx +++ b/apps/code/src/renderer/features/onboarding/components/StepIndicator.tsx @@ -10,11 +10,12 @@ export function StepIndicator({ currentStep, activeSteps, }: StepIndicatorProps) { - const currentIndex = activeSteps.indexOf(currentStep); + const displaySteps = activeSteps; + const currentIndex = displaySteps.indexOf(currentStep); return ( - {activeSteps.map((step, index) => ( + {displaySteps.map((step, index) => (
, + title: "Your signals inbox", + description: + "Automatically surfaces the highest-impact work from your product data so you always know what to do next.", + }, + { + icon: , + title: "Product data as context", + description: + "Your agents have context from your analytics, session replays and feature flags built in.", + }, + { + icon: , + title: "Any model, any harness", + description: + "Bring your own agent framework or use our built-in harnesses. Swap models without changing your workflow.", + }, + { + icon: , + title: "Ship work, not messages", + description: + "Run tasks in parallel across local and cloud environments. Work gets done whether you're watching or not.", + }, + { + icon: , + title: "Review and ship with confidence", + description: + "Inline diffs, AI-assisted code review and automated pull request creation in one flow.", + }, +]; + +interface WelcomeScreenProps { + onNext: () => void; +} + +const CYCLE_INTERVAL_MS = 2500; +const CYCLE_START_DELAY_MS = FEATURES.length * 100 + 400; + +export function WelcomeScreen({ onNext }: WelcomeScreenProps) { + const [activeIndex, setActiveIndex] = useState(-1); + const timerRef = useRef>(null); + + const startCycling = useCallback(() => { + if (timerRef.current) clearInterval(timerRef.current); + timerRef.current = setInterval(() => { + setActiveIndex((prev) => (prev + 1) % FEATURES.length); + }, CYCLE_INTERVAL_MS); + }, []); + + useEffect(() => { + const timeout = setTimeout(() => { + setActiveIndex(0); + startCycling(); + }, CYCLE_START_DELAY_MS); + + return () => { + clearTimeout(timeout); + if (timerRef.current) clearInterval(timerRef.current); + }; + }, [startCycling]); + + const handleMouseEnter = (index: number) => { + setActiveIndex(index); + if (timerRef.current) clearInterval(timerRef.current); + }; + + const handleMouseLeave = () => { + startCycling(); + }; + + return ( + + + + + + + Welcome to PostHog Code + + + Your product workbench. + + + + + {FEATURES.map((feature, index) => ( + handleMouseEnter(index)} + onMouseLeave={handleMouseLeave} + /> + ))} + + + + + + + + + + + + ); +} diff --git a/apps/code/src/renderer/features/onboarding/components/WelcomeStep.tsx b/apps/code/src/renderer/features/onboarding/components/WelcomeStep.tsx deleted file mode 100644 index b9b0c1d7e..000000000 --- a/apps/code/src/renderer/features/onboarding/components/WelcomeStep.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { - ArrowRight, - Cloud, - CodeBlock, - GitPullRequest, - Robot, - Stack, -} from "@phosphor-icons/react"; -import { Button, Flex } from "@radix-ui/themes"; -import codeLogo from "@renderer/assets/images/code.svg"; -import { FeatureListItem } from "./FeatureListItem"; - -interface WelcomeStepProps { - onNext: () => void; -} - -const FEATURES = [ - { - icon: , - title: "Use any agent or harness", - description: - "Bring your own agent framework or use our built-in harnesses to get started fast.", - }, - { - icon: , - title: "Run your agent anywhere", - description: - "Work locally, in a worktree, or spin up cloud environments on demand.", - }, - { - icon: , - title: "Review your code", - description: - "Inline diffs, focused reviews, and AI-assisted code understanding.", - }, - { - icon: , - title: "Create pull requests", - description: - "Go from task to PR with automated branch management and descriptions.", - }, - { - icon: , - title: "Run many agents at once", - description: - "Parallelise work across multiple agents tackling different tasks simultaneously.", - }, -]; - -export function WelcomeStep({ onNext }: WelcomeStepProps) { - return ( - - - - PostHog Code - - - - - {FEATURES.map((feature) => ( - - ))} - - - - - - - - - ); -} diff --git a/apps/code/src/renderer/features/onboarding/hooks/useOnboardingFlow.ts b/apps/code/src/renderer/features/onboarding/hooks/useOnboardingFlow.ts index a0b0bfbbb..68956f06b 100644 --- a/apps/code/src/renderer/features/onboarding/hooks/useOnboardingFlow.ts +++ b/apps/code/src/renderer/features/onboarding/hooks/useOnboardingFlow.ts @@ -1,11 +1,96 @@ +import { useAuthStateValue } from "@features/auth/hooks/authQueries"; import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore"; +import { trpcClient } from "@renderer/trpc/client"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ONBOARDING_STEPS, type OnboardingStep } from "../types"; +export interface DetectedRepo { + organization: string; + repository: string; + fullName: string; + remote?: string; + branch?: string; +} + export function useOnboardingFlow() { const currentStep = useOnboardingStore((state) => state.currentStep); const setCurrentStep = useOnboardingStore((state) => state.setCurrentStep); + const selectedDirectory = useOnboardingStore( + (state) => state.selectedDirectory, + ); + const setSelectedDirectory = useOnboardingStore( + (state) => state.setSelectedDirectory, + ); + const directionRef = useRef<1 | -1>(1); + + const [detectedRepo, setDetectedRepo] = useState(null); + const [isDetectingRepo, setIsDetectingRepo] = useState(false); + const hasRehydrated = useRef(false); + + useEffect(() => { + if (hasRehydrated.current || !selectedDirectory) return; + hasRehydrated.current = true; + setIsDetectingRepo(true); + trpcClient.git.detectRepo + .query({ directoryPath: selectedDirectory }) + .then((result) => { + if (result) { + setDetectedRepo({ + organization: result.organization, + repository: result.repository, + fullName: `${result.organization}/${result.repository}`, + remote: result.remote ?? undefined, + branch: result.branch ?? undefined, + }); + } + }) + .catch(() => {}) + .finally(() => setIsDetectingRepo(false)); + }, [selectedDirectory]); + + const handleDirectoryChange = useCallback( + async (path: string) => { + setSelectedDirectory(path); + setDetectedRepo(null); + if (!path) return; - const activeSteps = ONBOARDING_STEPS; + setIsDetectingRepo(true); + try { + const result = await trpcClient.git.detectRepo.query({ + directoryPath: path, + }); + if (result) { + setDetectedRepo({ + organization: result.organization, + repository: result.repository, + fullName: `${result.organization}/${result.repository}`, + remote: result.remote ?? undefined, + branch: result.branch ?? undefined, + }); + } + } catch { + // Not a git repo or no remote + } finally { + setIsDetectingRepo(false); + } + }, + [setSelectedDirectory], + ); + + const hasCodeAccess = useAuthStateValue((state) => state.hasCodeAccess); + + const activeSteps = useMemo(() => { + if (hasCodeAccess === true) { + return ONBOARDING_STEPS.filter((s) => s !== "invite-code"); + } + return ONBOARDING_STEPS; + }, [hasCodeAccess]); + + useEffect(() => { + if (!activeSteps.includes(currentStep)) { + setCurrentStep(activeSteps[0]); + } + }, [activeSteps, currentStep, setCurrentStep]); const currentIndex = activeSteps.indexOf(currentStep); const isFirstStep = currentIndex === 0; @@ -13,17 +98,21 @@ export function useOnboardingFlow() { const next = () => { if (!isLastStep) { + directionRef.current = 1; setCurrentStep(activeSteps[currentIndex + 1]); } }; const back = () => { if (!isFirstStep) { + directionRef.current = -1; setCurrentStep(activeSteps[currentIndex - 1]); } }; const goTo = (step: OnboardingStep) => { + const targetIndex = activeSteps.indexOf(step); + directionRef.current = targetIndex >= currentIndex ? 1 : -1; setCurrentStep(step); }; @@ -34,8 +123,13 @@ export function useOnboardingFlow() { activeSteps, isFirstStep, isLastStep, + direction: directionRef.current, next, back, goTo, + selectedDirectory, + detectedRepo, + isDetectingRepo, + handleDirectoryChange, }; } diff --git a/apps/code/src/renderer/features/onboarding/hooks/usePrefetchSignalData.ts b/apps/code/src/renderer/features/onboarding/hooks/usePrefetchSignalData.ts new file mode 100644 index 000000000..5f65d593a --- /dev/null +++ b/apps/code/src/renderer/features/onboarding/hooks/usePrefetchSignalData.ts @@ -0,0 +1,55 @@ +import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient"; +import { useAuthStateValue } from "@features/auth/hooks/authQueries"; +import { useQueryClient } from "@tanstack/react-query"; +import { useEffect } from "react"; + +/** + * Prefetches onboarding step data so GitHub and Signals steps load instantly. + * Call this early in the onboarding flow (e.g. in OnboardingFlow component). + */ +export function usePrefetchSignalData(): void { + const client = useOptionalAuthenticatedClient(); + const projectId = useAuthStateValue((state) => state.projectId); + const queryClient = useQueryClient(); + + useEffect(() => { + if (!client || !projectId) return; + + queryClient.prefetchQuery({ + queryKey: ["integrations", projectId], + queryFn: () => client.getIntegrationsForProject(projectId), + staleTime: 60_000, + }); + + queryClient.prefetchQuery({ + queryKey: ["signals", "source-configs", projectId], + queryFn: () => client.listSignalSourceConfigs(projectId), + staleTime: 30_000, + }); + + queryClient.prefetchQuery({ + queryKey: ["external-data-sources", projectId], + queryFn: () => client.listExternalDataSources(projectId), + staleTime: 60_000, + }); + + queryClient.prefetchQuery({ + queryKey: ["integrations", "list"], + queryFn: async () => { + const integrations = await client.getIntegrations(); + const ghIntegration = ( + integrations as { id: number; kind: string }[] + ).find((i) => i.kind === "github"); + if (ghIntegration) { + queryClient.prefetchQuery({ + queryKey: ["integrations", "repositories", ghIntegration.id], + queryFn: () => client.getGithubRepositories(ghIntegration.id), + staleTime: 60_000, + }); + } + return integrations; + }, + staleTime: 60_000, + }); + }, [client, projectId, queryClient]); +} diff --git a/apps/code/src/renderer/features/onboarding/hooks/useProjectsWithIntegrations.ts b/apps/code/src/renderer/features/onboarding/hooks/useProjectsWithIntegrations.ts index e441f606a..c6da7b49e 100644 --- a/apps/code/src/renderer/features/onboarding/hooks/useProjectsWithIntegrations.ts +++ b/apps/code/src/renderer/features/onboarding/hooks/useProjectsWithIntegrations.ts @@ -1,4 +1,4 @@ -import { useAuthenticatedClient } from "@features/auth/hooks/authClient"; +import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient"; import { AUTH_SCOPED_QUERY_META } from "@features/auth/hooks/authQueries"; import type { Integration } from "@features/integrations/stores/integrationStore"; import { useProjects } from "@features/projects/hooks/useProjects"; @@ -15,7 +15,7 @@ export interface ProjectWithIntegrations { export function useProjectsWithIntegrations() { const { projects, isLoading: projectsLoading } = useProjects(); - const client = useAuthenticatedClient(); + const client = useOptionalAuthenticatedClient(); // Fetch integrations for each project in parallel const integrationQueries = useQueries({ diff --git a/apps/code/src/renderer/features/onboarding/stores/onboardingStore.ts b/apps/code/src/renderer/features/onboarding/stores/onboardingStore.ts index d5487c96c..c2563bc63 100644 --- a/apps/code/src/renderer/features/onboarding/stores/onboardingStore.ts +++ b/apps/code/src/renderer/features/onboarding/stores/onboardingStore.ts @@ -9,8 +9,8 @@ interface OnboardingStoreState { currentStep: OnboardingStep; hasCompletedOnboarding: boolean; isConnectingGithub: boolean; - selectedOrgId: string | null; selectedProjectId: number | null; + selectedDirectory: string; } interface OnboardingStoreActions { @@ -19,8 +19,8 @@ interface OnboardingStoreActions { resetOnboarding: () => void; resetSelections: () => void; setConnectingGithub: (isConnecting: boolean) => void; - selectOrg: (orgId: string) => void; selectProjectId: (projectId: number | null) => void; + setSelectedDirectory: (path: string) => void; } type OnboardingStore = OnboardingStoreState & OnboardingStoreActions; @@ -29,8 +29,8 @@ const initialState: OnboardingStoreState = { currentStep: "welcome", hasCompletedOnboarding: false, isConnectingGithub: false, - selectedOrgId: null, selectedProjectId: null, + selectedDirectory: "", }; export const useOnboardingStore = create()( @@ -48,18 +48,19 @@ export const useOnboardingStore = create()( set({ currentStep: "welcome", isConnectingGithub: false, - selectedOrgId: null, selectedProjectId: null, }), setConnectingGithub: (isConnectingGithub) => set({ isConnectingGithub }), - selectOrg: (orgId) => set({ selectedOrgId: orgId }), selectProjectId: (selectedProjectId) => set({ selectedProjectId }), + setSelectedDirectory: (selectedDirectory) => set({ selectedDirectory }), }), { name: "onboarding-store", partialize: (state) => ({ currentStep: state.currentStep, hasCompletedOnboarding: state.hasCompletedOnboarding, + selectedProjectId: state.selectedProjectId, + selectedDirectory: state.selectedDirectory, }), }, ), diff --git a/apps/code/src/renderer/features/onboarding/types.ts b/apps/code/src/renderer/features/onboarding/types.ts index b66109311..de3eef5b0 100644 --- a/apps/code/src/renderer/features/onboarding/types.ts +++ b/apps/code/src/renderer/features/onboarding/types.ts @@ -1,8 +1,16 @@ -export type OnboardingStep = "welcome" | "org" | "git-integration" | "signals"; +export type OnboardingStep = + | "welcome" + | "project-select" + | "invite-code" + | "github" + | "install-cli" + | "signals"; export const ONBOARDING_STEPS: OnboardingStep[] = [ "welcome", - "org", - "git-integration", + "project-select", + "invite-code", + "github", + "install-cli", "signals", ]; diff --git a/apps/code/src/renderer/features/projects/hooks/useProjects.tsx b/apps/code/src/renderer/features/projects/hooks/useProjects.tsx index e7406bcc0..d9bf2bd1b 100644 --- a/apps/code/src/renderer/features/projects/hooks/useProjects.tsx +++ b/apps/code/src/renderer/features/projects/hooks/useProjects.tsx @@ -84,18 +84,34 @@ export function useProjects() { const currentProject = projects.find((p) => p.id === currentProjectId); const groupedProjects = groupProjectsByOrg(projects); + const userTeamId = + currentUser?.team && typeof currentUser.team === "object" + ? (currentUser.team as { id: number }).id + : null; + useEffect(() => { if (projects.length > 0 && !currentProject) { - log.info("Auto-selecting first available project", { - projectId: projects[0].id, + const preferredProject = + (userTeamId && projects.find((p) => p.id === userTeamId)) || + projects[0]; + log.info("Auto-selecting project", { + projectId: preferredProject.id, + source: + preferredProject.id === userTeamId ? "user-team" : "first-available", reason: currentProjectId == null ? "no project selected" : "current project not found in list", }); - selectProjectMutation.mutate(projects[0].id); + selectProjectMutation.mutate(preferredProject.id); } - }, [currentProject, currentProjectId, projects, selectProjectMutation]); + }, [ + currentProject, + currentProjectId, + projects, + selectProjectMutation, + userTeamId, + ]); return { projects, diff --git a/apps/code/src/renderer/features/settings/components/sections/SignalSourcesSettings.tsx b/apps/code/src/renderer/features/settings/components/sections/SignalSourcesSettings.tsx index 7176df87e..773f9d162 100644 --- a/apps/code/src/renderer/features/settings/components/sections/SignalSourcesSettings.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/SignalSourcesSettings.tsx @@ -3,7 +3,6 @@ import { SignalSourceToggles } from "@features/inbox/components/SignalSourceTogg import { useSignalSourceManager } from "@features/inbox/hooks/useSignalSourceManager"; import { GitHubIntegrationSection } from "@features/settings/components/sections/GitHubIntegrationSection"; import { useRepositoryIntegration } from "@hooks/useIntegrations"; -import { useMeQuery } from "@hooks/useMeQuery"; import { Box, Flex, Select, Text, Tooltip } from "@radix-ui/themes"; import type { SignalReportPriority } from "@shared/types"; @@ -32,16 +31,11 @@ export function SignalSourcesSettings() { handleSetup, handleSetupComplete, handleSetupCancel, - evaluations, - evaluationsUrl, - handleToggleEvaluation, userAutonomyConfig, handleUpdateUserAutonomyPriority, } = useSignalSourceManager(); const { hasGithubIntegration } = useRepositoryIntegration(); - const { data: me } = useMeQuery(); - const isStaff = me?.is_staff ?? false; if (isLoading) { return ( @@ -90,13 +84,6 @@ export function SignalSourcesSettings() { disabled={!hasGithubIntegration} sourceStates={sourceStates} onSetup={handleSetup} - evaluations={isStaff ? evaluations : undefined} - evaluationsUrl={isStaff ? evaluationsUrl : undefined} - onToggleEvaluation={ - isStaff - ? (id, enabled) => void handleToggleEvaluation(id, enabled) - : undefined - } /> )} diff --git a/apps/code/src/renderer/features/sidebar/components/ProjectSwitcher.tsx b/apps/code/src/renderer/features/sidebar/components/ProjectSwitcher.tsx index 24e012bd3..18fbfddcb 100644 --- a/apps/code/src/renderer/features/sidebar/components/ProjectSwitcher.tsx +++ b/apps/code/src/renderer/features/sidebar/components/ProjectSwitcher.tsx @@ -21,6 +21,7 @@ import { import { Box, Dialog, Flex, Text } from "@radix-ui/themes"; import { trpcClient } from "@renderer/trpc/client"; import { getCloudUrlFromRegion } from "@shared/utils/urls"; +import { EXTERNAL_LINKS } from "@utils/links"; import { isMac } from "@utils/platform"; import { useState } from "react"; import "./ProjectSwitcher.css"; @@ -95,7 +96,7 @@ export function ProjectSwitcher() { const handleDiscord = async () => { await trpcClient.os.openExternal.mutate({ - url: "https://discord.gg/c3qYyJXSWp", + url: EXTERNAL_LINKS.discord, }); setPopoverOpen(false); }; @@ -184,18 +185,14 @@ export function ProjectSwitcher() { - handleOpenExternal("https://posthog.com/code") - } + onClick={() => handleOpenExternal(EXTERNAL_LINKS.website)} > PostHog Code Website - handleOpenExternal("https://posthog.com/privacy") - } + onClick={() => handleOpenExternal(EXTERNAL_LINKS.privacy)} > Privacy Policy diff --git a/apps/code/src/renderer/features/sidebar/components/TaskListView.tsx b/apps/code/src/renderer/features/sidebar/components/TaskListView.tsx index 28194039d..820fe3be6 100644 --- a/apps/code/src/renderer/features/sidebar/components/TaskListView.tsx +++ b/apps/code/src/renderer/features/sidebar/components/TaskListView.tsx @@ -3,9 +3,8 @@ import type { DragDropEvents } from "@dnd-kit/react"; import { DragDropProvider } from "@dnd-kit/react"; import { useFolders } from "@features/folders/hooks/useFolders"; import { - FolderOpenIcon, - FolderSimple, FunnelSimple as FunnelSimpleIcon, + GitBranch, } from "@phosphor-icons/react"; import { Button, @@ -321,13 +320,7 @@ export function TaskListView({ - ) : ( - - ) - } + icon={} isExpanded={isExpanded} onToggle={() => toggleSection(group.id)} addSpacingBefore={false} 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 e60438cfd..60bc232c4 100644 --- a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx @@ -1,3 +1,4 @@ +import { DotPatternBackground } from "@components/DotPatternBackground"; import { EnvironmentSelector } from "@features/environments/components/EnvironmentSelector"; import { FolderPicker } from "@features/folder-picker/components/FolderPicker"; import { GitHubRepoPicker } from "@features/folder-picker/components/GitHubRepoPicker"; @@ -38,8 +39,6 @@ import { usePreviewConfig } from "../hooks/usePreviewConfig"; import { useTaskCreation } from "../hooks/useTaskCreation"; import { type WorkspaceMode, WorkspaceModeSelect } from "./WorkspaceModeSelect"; -const DOT_FILL = "var(--gray-6)"; - interface TaskInputProps { sessionId?: string; onTaskCreated?: (task: import("@shared/types").Task) => void; @@ -417,37 +416,7 @@ export function TaskInput({ height="100%" style={{ position: "relative" }} > - + ({ @@ -57,7 +57,7 @@ function useAllGithubRepositories(githubIntegrations: Integration[]) { for (const result of results) { if (result.isPending) pending = true; if (!result.data) continue; - for (const repo of result.data.repos) { + for (const repo of result.data.repos ?? []) { if (!(repo in map)) { map[repo] = result.data.integrationId; } diff --git a/apps/code/src/renderer/hooks/useOrganizations.ts b/apps/code/src/renderer/hooks/useOrganizations.ts deleted file mode 100644 index 6369fb1d5..000000000 --- a/apps/code/src/renderer/hooks/useOrganizations.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient"; -import { useCurrentUser } from "@features/auth/hooks/authQueries"; -import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore"; -import { useAuthenticatedQuery } from "@hooks/useAuthenticatedQuery"; -import type { PostHogAPIClient } from "@renderer/api/posthogClient"; -import { useMemo } from "react"; - -export interface OrgInfo { - id: string; - name: string; - slug: string; -} - -const organizationKeys = { - all: ["organizations"] as const, - list: () => [...organizationKeys.all, "list"] as const, -}; - -async function fetchOrgs(client: PostHogAPIClient): Promise { - const user = await client.getCurrentUser(); - return (user.organizations ?? []).map( - (org: { id: string; name: string; slug: string }) => ({ - id: org.id, - name: org.name, - slug: org.slug, - }), - ); -} - -export function useOrganizations() { - const selectedOrgId = useOnboardingStore((state) => state.selectedOrgId); - const client = useOptionalAuthenticatedClient(); - const { data: currentUser } = useCurrentUser({ client }); - - const { - data: orgs, - isLoading, - error, - } = useAuthenticatedQuery( - organizationKeys.list(), - (client) => fetchOrgs(client), - { staleTime: 5 * 60 * 1000 }, - ); - - const effectiveSelectedOrgId = useMemo(() => { - if (selectedOrgId) return selectedOrgId; - if (!orgs?.length) return null; - - const userCurrentOrgId = currentUser?.organization?.id; - if (userCurrentOrgId && orgs.some((org) => org.id === userCurrentOrgId)) { - return userCurrentOrgId; - } - - return orgs[0].id; - }, [currentUser?.organization?.id, orgs, selectedOrgId]); - - const sortedOrgs = useMemo(() => { - return [...(orgs ?? [])].sort((a, b) => a.name.localeCompare(b.name)); - }, [orgs]); - - return { - orgs: sortedOrgs, - effectiveSelectedOrgId, - isLoading, - error, - }; -} diff --git a/apps/code/src/renderer/utils/links.ts b/apps/code/src/renderer/utils/links.ts new file mode 100644 index 000000000..6eda52792 --- /dev/null +++ b/apps/code/src/renderer/utils/links.ts @@ -0,0 +1,7 @@ +export const EXTERNAL_LINKS = { + discord: "https://discord.gg/posthog", + ghInstall: "https://cli.github.com/", + gitInstall: "https://git-scm.com/downloads", + privacy: "https://posthog.com/privacy", + website: "https://posthog.com/code", +} as const; diff --git a/apps/code/tests/e2e/tests/smoke.spec.ts b/apps/code/tests/e2e/tests/smoke.spec.ts index 393e9309f..b442cbe65 100644 --- a/apps/code/tests/e2e/tests/smoke.spec.ts +++ b/apps/code/tests/e2e/tests/smoke.spec.ts @@ -20,6 +20,12 @@ test.describe("Smoke Tests", () => { .waitFor({ state: "hidden", timeout: 30000 }) .catch(() => {}); + const hasOnboarding = await window + .locator("text=Welcome to PostHog Code") + .first() + .isVisible() + .catch(() => false); + const hasAuthScreen = await window .locator("text=Sign in") .first() @@ -38,7 +44,8 @@ test.describe("Smoke Tests", () => { .isVisible() .catch(() => false); - const isValidBootState = hasAuthScreen || hasMainLayout || hasSettings; + const isValidBootState = + hasOnboarding || hasAuthScreen || hasMainLayout || hasSettings; expect(isValidBootState).toBe(true); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc3e99d73..c16c23455 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -169,12 +169,12 @@ importers: '@posthog/hedgehog-mode': specifier: ^0.0.48 version: 0.0.48(prop-types@15.8.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@posthog/quill': - specifier: 0.1.0-alpha.7 - version: 0.1.0-alpha.7(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tailwindcss@4.2.2) '@posthog/platform': specifier: workspace:* version: link:../../packages/platform + '@posthog/quill': + specifier: 0.1.0-alpha.7 + version: 0.1.0-alpha.7(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tailwindcss@4.2.2) '@posthog/shared': specifier: workspace:* version: link:../../packages/shared