From ff224028114d34eceb1ebd2f7cc7fb3d5334b4f4 Mon Sep 17 00:00:00 2001 From: PostHog Code Date: Wed, 22 Apr 2026 12:06:04 +0000 Subject: [PATCH 1/4] feat(inbox): gate signals inbox behind posthog-code-inbox flag Hides the sidebar inbox item and the inbox view when the posthog-code-inbox feature flag is disabled. Also skips the sidebar signal-count polling when the flag is off so we don't pay the network cost for users who can't see the inbox. Generated-By: PostHog Code Task-Id: f5d4a0ab-7de3-4bcb-a465-bbcf6cf7c22a --- .../src/renderer/components/MainLayout.tsx | 4 +++- .../sidebar/components/SidebarMenu.tsx | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/apps/code/src/renderer/components/MainLayout.tsx b/apps/code/src/renderer/components/MainLayout.tsx index 37cce59fe..87373facc 100644 --- a/apps/code/src/renderer/components/MainLayout.tsx +++ b/apps/code/src/renderer/components/MainLayout.tsx @@ -20,6 +20,7 @@ import { TourOverlay } from "@features/tour/components/TourOverlay"; import { useTourStore } from "@features/tour/stores/tourStore"; import { createFirstTaskTour } from "@features/tour/tours/createFirstTaskTour"; import { useConnectivity } from "@hooks/useConnectivity"; +import { useFeatureFlag } from "@hooks/useFeatureFlag"; import { useIntegrations } from "@hooks/useIntegrations"; import { Box, Flex } from "@radix-ui/themes"; import { useCommandMenuStore } from "@stores/commandMenuStore"; @@ -43,6 +44,7 @@ export function MainLayout() { } = useShortcutsSheetStore(); const { data: tasks } = useTasks(); const { showPrompt, isChecking, check, dismiss } = useConnectivity(); + const inboxEnabled = useFeatureFlag("posthog-code-inbox"); const startTour = useTourStore((s) => s.startTour); const isFirstTaskTourDone = useTourStore((s) => @@ -92,7 +94,7 @@ export function MainLayout() { {view.type === "folder-settings" && } - {view.type === "inbox" && } + {view.type === "inbox" && inboxEnabled && } {view.type === "archived" && } diff --git a/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx b/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx index 47222ff98..31f182b28 100644 --- a/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx +++ b/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx @@ -13,6 +13,7 @@ import { } from "@features/tasks/hooks/useArchiveTask"; import { useTasks, useUpdateTask } from "@features/tasks/hooks/useTasks"; import { useWorkspaces } from "@features/workspace/hooks/useWorkspace"; +import { useFeatureFlag } from "@hooks/useFeatureFlag"; import { useTaskContextMenu } from "@hooks/useTaskContextMenu"; import { ScrollArea, Separator } from "@posthog/quill"; import { Box, Flex } from "@radix-ui/themes"; @@ -55,10 +56,12 @@ function SidebarMenuComponent() { const sidebarData = useSidebarData({ activeView: view, }); + const inboxEnabled = useFeatureFlag("posthog-code-inbox"); const inboxPollingActive = useRendererWindowFocusStore((s) => s.focused); const { data: inboxProbe } = useInboxReports( { status: INBOX_PIPELINE_STATUS_FILTER }, { + enabled: inboxEnabled, refetchInterval: inboxPollingActive ? INBOX_REFETCH_INTERVAL_MS : false, refetchIntervalInBackground: false, staleTime: inboxPollingActive ? INBOX_REFETCH_INTERVAL_MS : 15_000, @@ -277,13 +280,15 @@ function SidebarMenuComponent() { /> - - - + {inboxEnabled && ( + + + + )} Date: Wed, 22 Apr 2026 16:06:27 +0100 Subject: [PATCH 2/4] feat(inbox): gate remaining inbox surfaces behind posthog-code-inbox flag (#1840) Co-authored-by: Cursor Agent --- .../components/GlobalEventHandlers.tsx | 8 +++++- .../components/KeyboardShortcutsSheet.tsx | 10 ++++++- .../onboarding/components/WelcomeScreen.tsx | 26 ++++++++++++++----- .../onboarding/hooks/useOnboardingFlow.ts | 12 ++++++--- .../settings/components/SettingsDialog.tsx | 11 +++++--- 5 files changed, 52 insertions(+), 15 deletions(-) diff --git a/apps/code/src/renderer/components/GlobalEventHandlers.tsx b/apps/code/src/renderer/components/GlobalEventHandlers.tsx index e1453521d..5b0776fbe 100644 --- a/apps/code/src/renderer/components/GlobalEventHandlers.tsx +++ b/apps/code/src/renderer/components/GlobalEventHandlers.tsx @@ -8,6 +8,7 @@ import { useSidebarStore } from "@features/sidebar/stores/sidebarStore"; import { useTasks } from "@features/tasks/hooks/useTasks"; import { useFocusWorkspace } from "@features/workspace/hooks/useFocusWorkspace"; import { useWorkspaces } from "@features/workspace/hooks/useWorkspace"; +import { useFeatureFlag } from "@hooks/useFeatureFlag"; import { SHORTCUTS } from "@renderer/constants/keyboard-shortcuts"; import { useTRPC } from "@renderer/trpc"; import type { Task } from "@shared/types"; @@ -170,10 +171,15 @@ export function GlobalEventHandlers({ setReviewMode(currentTaskId, mode === "closed" ? "split" : "closed"); }, [currentTaskId, getReviewMode, setReviewMode]); + const inboxEnabled = useFeatureFlag("posthog-code-inbox"); + useHotkeys(SHORTCUTS.TOGGLE_LEFT_SIDEBAR, toggleLeftSidebar, globalOptions); useHotkeys(SHORTCUTS.TOGGLE_REVIEW_PANEL, handleToggleReview, globalOptions); useHotkeys(SHORTCUTS.SHORTCUTS_SHEET, onToggleShortcutsSheet, globalOptions); - useHotkeys(SHORTCUTS.INBOX, navigateToInbox, globalOptions); + useHotkeys(SHORTCUTS.INBOX, navigateToInbox, { + ...globalOptions, + enabled: inboxEnabled, + }); useHotkeys(SHORTCUTS.PREV_TASK, handlePrevTask, globalOptions, [ handlePrevTask, ]); diff --git a/apps/code/src/renderer/components/KeyboardShortcutsSheet.tsx b/apps/code/src/renderer/components/KeyboardShortcutsSheet.tsx index 0699109aa..fee44a38e 100644 --- a/apps/code/src/renderer/components/KeyboardShortcutsSheet.tsx +++ b/apps/code/src/renderer/components/KeyboardShortcutsSheet.tsx @@ -1,3 +1,4 @@ +import { useFeatureFlag } from "@hooks/useFeatureFlag"; import { Box, Dialog, Flex, Text } from "@radix-ui/themes"; import { CATEGORY_LABELS, @@ -130,7 +131,14 @@ function ShortcutsHeader() { } export function KeyboardShortcutsList() { - const shortcutsByCategory = useMemo(() => getShortcutsByCategory(), []); + const inboxEnabled = useFeatureFlag("posthog-code-inbox"); + const shortcutsByCategory = useMemo(() => { + const grouped = getShortcutsByCategory(); + if (!inboxEnabled) { + grouped.navigation = grouped.navigation.filter((s) => s.id !== "inbox"); + } + return grouped; + }, [inboxEnabled]); const categoryOrder: ShortcutCategory[] = [ "general", diff --git a/apps/code/src/renderer/features/onboarding/components/WelcomeScreen.tsx b/apps/code/src/renderer/features/onboarding/components/WelcomeScreen.tsx index 0438932de..d939ea12d 100644 --- a/apps/code/src/renderer/features/onboarding/components/WelcomeScreen.tsx +++ b/apps/code/src/renderer/features/onboarding/components/WelcomeScreen.tsx @@ -1,3 +1,4 @@ +import { useFeatureFlag } from "@hooks/useFeatureFlag"; import { ArrowRight, ChartLine, @@ -8,37 +9,42 @@ import { } from "@phosphor-icons/react"; import { Button, Flex, Text } from "@radix-ui/themes"; import explorerHog from "@renderer/assets/images/hedgehogs/explorer-hog.png"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { FeatureListItem } from "./FeatureListItem"; import { OnboardingHogTip } from "./OnboardingHogTip"; import { StepActions } from "./StepActions"; -const FEATURES = [ +const ALL_FEATURES = [ { + id: "signals-inbox", icon: , title: "Your signals inbox", description: "Automatically surfaces the highest-impact work from your product data so you always know what to do next.", }, { + id: "product-data", icon: , title: "Product data as context", description: "Your agents have context from your analytics, session replays and feature flags built in.", }, { + id: "any-model", icon: , title: "Any model, any harness", description: "Bring your own agent framework or use our built-in harnesses. Swap models without changing your workflow.", }, { + id: "ship-work", 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.", }, { + id: "review-ship", icon: , title: "Review and ship with confidence", description: @@ -51,18 +57,26 @@ interface WelcomeScreenProps { } const CYCLE_INTERVAL_MS = 2500; -const CYCLE_START_DELAY_MS = FEATURES.length * 100 + 400; +const CYCLE_START_DELAY_MS = ALL_FEATURES.length * 100 + 400; export function WelcomeScreen({ onNext }: WelcomeScreenProps) { + const inboxEnabled = useFeatureFlag("posthog-code-inbox"); + const features = useMemo( + () => + inboxEnabled + ? ALL_FEATURES + : ALL_FEATURES.filter((f) => f.id !== "signals-inbox"), + [inboxEnabled], + ); 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); + setActiveIndex((prev) => (prev + 1) % features.length); }, CYCLE_INTERVAL_MS); - }, []); + }, [features.length]); useEffect(() => { const timeout = setTimeout(() => { @@ -126,7 +140,7 @@ export function WelcomeScreen({ onNext }: WelcomeScreenProps) { - {FEATURES.map((feature, index) => ( + {features.map((feature, index) => ( state.hasCodeAccess); + const inboxEnabled = useFeatureFlag("posthog-code-inbox"); const activeSteps = useMemo(() => { + let steps = ONBOARDING_STEPS as OnboardingStep[]; if (hasCodeAccess === true) { - return ONBOARDING_STEPS.filter((s) => s !== "invite-code"); + steps = steps.filter((s) => s !== "invite-code"); } - return ONBOARDING_STEPS; - }, [hasCodeAccess]); + if (!inboxEnabled) { + steps = steps.filter((s) => s !== "signals"); + } + return steps; + }, [hasCodeAccess, inboxEnabled]); useEffect(() => { if (!activeSteps.includes(currentStep)) { diff --git a/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx b/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx index 10ea83948..1b0cda973 100644 --- a/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx +++ b/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx @@ -129,14 +129,17 @@ export function SettingsDialog() { const { data: user } = useCurrentUser({ client }); const { seat, planLabel } = useSeat(); const billingEnabled = useFeatureFlag("posthog-code-billing"); + const inboxEnabled = useFeatureFlag("posthog-code-inbox"); const logoutMutation = useLogoutMutation(); const sidebarItems = useMemo( () => - billingEnabled - ? SIDEBAR_ITEMS - : SIDEBAR_ITEMS.filter((item) => item.id !== "plan-usage"), - [billingEnabled], + SIDEBAR_ITEMS.filter((item) => { + if (item.id === "plan-usage" && !billingEnabled) return false; + if (item.id === "signals" && !inboxEnabled) return false; + return true; + }), + [billingEnabled, inboxEnabled], ); useHotkeys("escape", close, { From accc99f50ceeecbe0523654ddb726afb6b6f73c5 Mon Sep 17 00:00:00 2001 From: Joshua Snyder Date: Wed, 22 Apr 2026 16:35:45 +0100 Subject: [PATCH 3/4] fix(onboarding): complete onboarding when last step continue is pressed (#1843) Co-authored-by: Cursor Agent --- .../onboarding/components/OnboardingFlow.tsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx b/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx index 2cb2ef939..c53b38abb 100644 --- a/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx +++ b/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx @@ -28,6 +28,7 @@ export function OnboardingFlow() { const { currentStep, activeSteps, + isLastStep, direction, next, back, @@ -46,12 +47,10 @@ export function OnboardingFlow() { ); usePrefetchSignalData(); - useHotkeys("right", next, { enableOnFormTags: false }, [next]); - useHotkeys("left", back, { enableOnFormTags: false }, [back]); + const handleNext = isLastStep ? completeOnboarding : next; - const handleComplete = () => { - completeOnboarding(); - }; + useHotkeys("right", handleNext, { enableOnFormTags: false }, [handleNext]); + useHotkeys("left", back, { enableOnFormTags: false }, [back]); const footerRight = ( @@ -75,7 +74,7 @@ export function OnboardingFlow() { size="1" variant="ghost" color="gray" - onClick={handleComplete} + onClick={completeOnboarding} style={{ opacity: 0.5 }} > @@ -100,7 +99,7 @@ export function OnboardingFlow() { transition={{ duration: 0.3 }} style={{ width: "100%", flex: 1, minHeight: 0 }} > - + )} @@ -115,7 +114,7 @@ export function OnboardingFlow() { transition={{ duration: 0.3 }} style={{ width: "100%", flex: 1, minHeight: 0 }} > - + )} @@ -130,7 +129,7 @@ export function OnboardingFlow() { transition={{ duration: 0.3 }} style={{ width: "100%", flex: 1, minHeight: 0 }} > - + )} @@ -146,7 +145,7 @@ export function OnboardingFlow() { style={{ width: "100%", flex: 1, minHeight: 0 }} > - + )} @@ -182,7 +181,7 @@ export function OnboardingFlow() { transition={{ duration: 0.3 }} style={{ width: "100%", flex: 1, minHeight: 0 }} > - + )} From 1c0c895b48050b3ee6b913ef0e67e1904ceddfc4 Mon Sep 17 00:00:00 2001 From: Joshua Snyder Date: Thu, 23 Apr 2026 13:24:38 +0100 Subject: [PATCH 4/4] refactor(inbox): rename flag to posthog-code-inbox-hidden with inverted logic (#1860) Co-authored-by: Cursor Agent --- .../src/renderer/components/GlobalEventHandlers.tsx | 4 ++-- .../src/renderer/components/KeyboardShortcutsSheet.tsx | 6 +++--- apps/code/src/renderer/components/MainLayout.tsx | 4 ++-- .../features/onboarding/components/WelcomeScreen.tsx | 10 +++++----- .../features/onboarding/hooks/useOnboardingFlow.ts | 6 +++--- .../features/settings/components/SettingsDialog.tsx | 6 +++--- .../features/sidebar/components/SidebarMenu.tsx | 6 +++--- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/code/src/renderer/components/GlobalEventHandlers.tsx b/apps/code/src/renderer/components/GlobalEventHandlers.tsx index 5b0776fbe..f4597643c 100644 --- a/apps/code/src/renderer/components/GlobalEventHandlers.tsx +++ b/apps/code/src/renderer/components/GlobalEventHandlers.tsx @@ -171,14 +171,14 @@ export function GlobalEventHandlers({ setReviewMode(currentTaskId, mode === "closed" ? "split" : "closed"); }, [currentTaskId, getReviewMode, setReviewMode]); - const inboxEnabled = useFeatureFlag("posthog-code-inbox"); + const inboxHidden = useFeatureFlag("posthog-code-inbox-hidden"); useHotkeys(SHORTCUTS.TOGGLE_LEFT_SIDEBAR, toggleLeftSidebar, globalOptions); useHotkeys(SHORTCUTS.TOGGLE_REVIEW_PANEL, handleToggleReview, globalOptions); useHotkeys(SHORTCUTS.SHORTCUTS_SHEET, onToggleShortcutsSheet, globalOptions); useHotkeys(SHORTCUTS.INBOX, navigateToInbox, { ...globalOptions, - enabled: inboxEnabled, + enabled: !inboxHidden, }); useHotkeys(SHORTCUTS.PREV_TASK, handlePrevTask, globalOptions, [ handlePrevTask, diff --git a/apps/code/src/renderer/components/KeyboardShortcutsSheet.tsx b/apps/code/src/renderer/components/KeyboardShortcutsSheet.tsx index fee44a38e..3e59f6984 100644 --- a/apps/code/src/renderer/components/KeyboardShortcutsSheet.tsx +++ b/apps/code/src/renderer/components/KeyboardShortcutsSheet.tsx @@ -131,14 +131,14 @@ function ShortcutsHeader() { } export function KeyboardShortcutsList() { - const inboxEnabled = useFeatureFlag("posthog-code-inbox"); + const inboxHidden = useFeatureFlag("posthog-code-inbox-hidden"); const shortcutsByCategory = useMemo(() => { const grouped = getShortcutsByCategory(); - if (!inboxEnabled) { + if (inboxHidden) { grouped.navigation = grouped.navigation.filter((s) => s.id !== "inbox"); } return grouped; - }, [inboxEnabled]); + }, [inboxHidden]); const categoryOrder: ShortcutCategory[] = [ "general", diff --git a/apps/code/src/renderer/components/MainLayout.tsx b/apps/code/src/renderer/components/MainLayout.tsx index e35b7d8a4..901b93ba2 100644 --- a/apps/code/src/renderer/components/MainLayout.tsx +++ b/apps/code/src/renderer/components/MainLayout.tsx @@ -51,7 +51,7 @@ export function MainLayout() { } = useShortcutsSheetStore(); const { data: tasks } = useTasks(); const { showPrompt, isChecking, check, dismiss } = useConnectivity(); - const inboxEnabled = useFeatureFlag("posthog-code-inbox"); + const inboxHidden = useFeatureFlag("posthog-code-inbox-hidden"); const billingEnabled = useFeatureFlag(BILLING_FLAG); // Space switcher data @@ -109,7 +109,7 @@ export function MainLayout() { {view.type === "folder-settings" && } - {view.type === "inbox" && inboxEnabled && } + {view.type === "inbox" && !inboxHidden && } {view.type === "archived" && } diff --git a/apps/code/src/renderer/features/onboarding/components/WelcomeScreen.tsx b/apps/code/src/renderer/features/onboarding/components/WelcomeScreen.tsx index d939ea12d..c10177daf 100644 --- a/apps/code/src/renderer/features/onboarding/components/WelcomeScreen.tsx +++ b/apps/code/src/renderer/features/onboarding/components/WelcomeScreen.tsx @@ -60,13 +60,13 @@ const CYCLE_INTERVAL_MS = 2500; const CYCLE_START_DELAY_MS = ALL_FEATURES.length * 100 + 400; export function WelcomeScreen({ onNext }: WelcomeScreenProps) { - const inboxEnabled = useFeatureFlag("posthog-code-inbox"); + const inboxHidden = useFeatureFlag("posthog-code-inbox-hidden"); const features = useMemo( () => - inboxEnabled - ? ALL_FEATURES - : ALL_FEATURES.filter((f) => f.id !== "signals-inbox"), - [inboxEnabled], + inboxHidden + ? ALL_FEATURES.filter((f) => f.id !== "signals-inbox") + : ALL_FEATURES, + [inboxHidden], ); const [activeIndex, setActiveIndex] = useState(-1); const timerRef = useRef>(null); diff --git a/apps/code/src/renderer/features/onboarding/hooks/useOnboardingFlow.ts b/apps/code/src/renderer/features/onboarding/hooks/useOnboardingFlow.ts index 9cac62ad8..ac5c385d6 100644 --- a/apps/code/src/renderer/features/onboarding/hooks/useOnboardingFlow.ts +++ b/apps/code/src/renderer/features/onboarding/hooks/useOnboardingFlow.ts @@ -79,18 +79,18 @@ export function useOnboardingFlow() { ); const hasCodeAccess = useAuthStateValue((state) => state.hasCodeAccess); - const inboxEnabled = useFeatureFlag("posthog-code-inbox"); + const inboxHidden = useFeatureFlag("posthog-code-inbox-hidden"); const activeSteps = useMemo(() => { let steps = ONBOARDING_STEPS as OnboardingStep[]; if (hasCodeAccess === true) { steps = steps.filter((s) => s !== "invite-code"); } - if (!inboxEnabled) { + if (inboxHidden) { steps = steps.filter((s) => s !== "signals"); } return steps; - }, [hasCodeAccess, inboxEnabled]); + }, [hasCodeAccess, inboxHidden]); useEffect(() => { if (!activeSteps.includes(currentStep)) { diff --git a/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx b/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx index 9b3c15930..29cbc55ed 100644 --- a/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx +++ b/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx @@ -129,7 +129,7 @@ export function SettingsDialog() { const client = useOptionalAuthenticatedClient(); const { data: user } = useCurrentUser({ client }); const { seat, planLabel } = useSeat(); - const inboxEnabled = useFeatureFlag("posthog-code-inbox"); + const inboxHidden = useFeatureFlag("posthog-code-inbox-hidden"); const billingEnabled = useFeatureFlag(BILLING_FLAG); const logoutMutation = useLogoutMutation(); @@ -137,10 +137,10 @@ export function SettingsDialog() { () => SIDEBAR_ITEMS.filter((item) => { if (item.id === "plan-usage" && !billingEnabled) return false; - if (item.id === "signals" && !inboxEnabled) return false; + if (item.id === "signals" && inboxHidden) return false; return true; }), - [billingEnabled, inboxEnabled], + [billingEnabled, inboxHidden], ); useHotkeys("escape", close, { diff --git a/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx b/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx index 31f182b28..ce1e2d1e5 100644 --- a/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx +++ b/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx @@ -56,12 +56,12 @@ function SidebarMenuComponent() { const sidebarData = useSidebarData({ activeView: view, }); - const inboxEnabled = useFeatureFlag("posthog-code-inbox"); + const inboxHidden = useFeatureFlag("posthog-code-inbox-hidden"); const inboxPollingActive = useRendererWindowFocusStore((s) => s.focused); const { data: inboxProbe } = useInboxReports( { status: INBOX_PIPELINE_STATUS_FILTER }, { - enabled: inboxEnabled, + enabled: !inboxHidden, refetchInterval: inboxPollingActive ? INBOX_REFETCH_INTERVAL_MS : false, refetchIntervalInBackground: false, staleTime: inboxPollingActive ? INBOX_REFETCH_INTERVAL_MS : 15_000, @@ -280,7 +280,7 @@ function SidebarMenuComponent() { /> - {inboxEnabled && ( + {!inboxHidden && (