From 9b082324b030eef411f4443f32a131e24fe3e525 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Mon, 20 Apr 2026 20:47:13 -0700 Subject: [PATCH 1/3] Add usage limit detection and upgrade prompt --- .../src/renderer/components/MainLayout.tsx | 6 +++ .../billing/components/SidebarUsageBar.tsx | 46 ++++++++++++++++++ .../billing/components/UsageLimitModal.tsx | 47 +++++++++++++++++++ .../features/billing/hooks/useUsage.ts | 16 +++++++ .../billing/hooks/useUsageLimitDetection.ts | 46 ++++++++++++++++++ .../billing/stores/usageLimitStore.ts | 23 +++++++++ .../components/sections/PlanUsageSettings.tsx | 11 +---- .../sidebar/components/SidebarContent.tsx | 4 ++ 8 files changed, 189 insertions(+), 10 deletions(-) create mode 100644 apps/code/src/renderer/features/billing/components/SidebarUsageBar.tsx create mode 100644 apps/code/src/renderer/features/billing/components/UsageLimitModal.tsx create mode 100644 apps/code/src/renderer/features/billing/hooks/useUsage.ts create mode 100644 apps/code/src/renderer/features/billing/hooks/useUsageLimitDetection.ts create mode 100644 apps/code/src/renderer/features/billing/stores/usageLimitStore.ts diff --git a/apps/code/src/renderer/components/MainLayout.tsx b/apps/code/src/renderer/components/MainLayout.tsx index f49e76d66..a87daff07 100644 --- a/apps/code/src/renderer/components/MainLayout.tsx +++ b/apps/code/src/renderer/components/MainLayout.tsx @@ -4,6 +4,8 @@ import { HedgehogMode } from "@components/HedgehogMode"; import { KeyboardShortcutsSheet } from "@components/KeyboardShortcutsSheet"; import { ArchivedTasksView } from "@features/archive/components/ArchivedTasksView"; +import { UsageLimitModal } from "@features/billing/components/UsageLimitModal"; +import { useUsageLimitDetection } from "@features/billing/hooks/useUsageLimitDetection"; import { CommandMenu } from "@features/command/components/CommandMenu"; import { CommandCenterView } from "@features/command-center/components/CommandCenterView"; import { InboxView } from "@features/inbox/components/InboxView"; @@ -15,6 +17,7 @@ import { TaskDetail } from "@features/task-detail/components/TaskDetail"; import { TaskInput } from "@features/task-detail/components/TaskInput"; import { useTasks } from "@features/tasks/hooks/useTasks"; 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"; @@ -38,7 +41,9 @@ export function MainLayout() { } = useShortcutsSheetStore(); const { data: tasks } = useTasks(); const { showPrompt, isChecking, check, dismiss } = useConnectivity(); + const billingEnabled = useFeatureFlag("posthog-code-billing"); + useUsageLimitDetection(); useIntegrations(); useTaskDeepLink(); @@ -99,6 +104,7 @@ export function MainLayout() { onToggleShortcutsSheet={toggleShortcutsSheet} /> + {billingEnabled && } ); diff --git a/apps/code/src/renderer/features/billing/components/SidebarUsageBar.tsx b/apps/code/src/renderer/features/billing/components/SidebarUsageBar.tsx new file mode 100644 index 000000000..312aa8bf9 --- /dev/null +++ b/apps/code/src/renderer/features/billing/components/SidebarUsageBar.tsx @@ -0,0 +1,46 @@ +import { useUsage } from "@features/billing/hooks/useUsage"; +import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore"; +import { useSeat } from "@hooks/useSeat"; +import { Box, Flex, Progress, Text } from "@radix-ui/themes"; + +export function SidebarUsageBar() { + const { usage } = useUsage(); + const { isPro } = useSeat(); + + if (isPro || !usage) return null; + + const usagePercent = Math.max( + usage.sustained.used_percent, + usage.burst.used_percent, + ); + const exceeded = + usage.is_rate_limited || usage.sustained.exceeded || usage.burst.exceeded; + + const handleUpgrade = () => { + useSettingsDialogStore.getState().open("plan-usage"); + }; + + return ( + + + + + {exceeded ? "Limit reached" : `${Math.round(usagePercent)}% used`} + + + + + + + ); +} diff --git a/apps/code/src/renderer/features/billing/components/UsageLimitModal.tsx b/apps/code/src/renderer/features/billing/components/UsageLimitModal.tsx new file mode 100644 index 000000000..f15a1eed4 --- /dev/null +++ b/apps/code/src/renderer/features/billing/components/UsageLimitModal.tsx @@ -0,0 +1,47 @@ +import { useUsageLimitStore } from "@features/billing/stores/usageLimitStore"; +import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore"; +import { WarningCircle } from "@phosphor-icons/react"; +import { Button, Dialog, Flex, Text } from "@radix-ui/themes"; + +export function UsageLimitModal() { + const isOpen = useUsageLimitStore((s) => s.isOpen); + const context = useUsageLimitStore((s) => s.context); + const hide = useUsageLimitStore((s) => s.hide); + + const handleUpgrade = () => { + hide(); + useSettingsDialogStore.getState().open("plan-usage"); + }; + + return ( + + e.preventDefault()} + onInteractOutside={(e) => e.preventDefault()} + > + + + + Usage limit reached + + + + {context === "mid-task" + ? "You've hit your free plan usage limit. Your current task can't continue until usage resets or you upgrade to Pro." + : "You've reached your free plan usage limit. Upgrade to Pro for unlimited usage."} + + + + + + + + + + ); +} diff --git a/apps/code/src/renderer/features/billing/hooks/useUsage.ts b/apps/code/src/renderer/features/billing/hooks/useUsage.ts new file mode 100644 index 000000000..e943e51c7 --- /dev/null +++ b/apps/code/src/renderer/features/billing/hooks/useUsage.ts @@ -0,0 +1,16 @@ +import { useTRPC } from "@renderer/trpc"; +import { useRendererWindowFocusStore } from "@stores/rendererWindowFocusStore"; +import { useQuery } from "@tanstack/react-query"; + +const USAGE_REFETCH_INTERVAL_MS = 60_000; + +export function useUsage() { + const trpc = useTRPC(); + const focused = useRendererWindowFocusStore((s) => s.focused); + const { data: usage, isLoading } = useQuery({ + ...trpc.llmGateway.usage.queryOptions(), + refetchInterval: focused ? USAGE_REFETCH_INTERVAL_MS : false, + refetchIntervalInBackground: false, + }); + return { usage: usage ?? null, isLoading }; +} diff --git a/apps/code/src/renderer/features/billing/hooks/useUsageLimitDetection.ts b/apps/code/src/renderer/features/billing/hooks/useUsageLimitDetection.ts new file mode 100644 index 000000000..1e47c526c --- /dev/null +++ b/apps/code/src/renderer/features/billing/hooks/useUsageLimitDetection.ts @@ -0,0 +1,46 @@ +import { useUsageLimitStore } from "@features/billing/stores/usageLimitStore"; +import { useSessionStore } from "@features/sessions/stores/sessionStore"; +import { useFeatureFlag } from "@hooks/useFeatureFlag"; +import { useSeat } from "@hooks/useSeat"; +import { useEffect, useRef } from "react"; +import { useUsage } from "./useUsage"; + +function isExceeded(usage: { + sustained: { exceeded: boolean }; + burst: { exceeded: boolean }; + is_rate_limited: boolean; +}): boolean { + return ( + usage.is_rate_limited || usage.sustained.exceeded || usage.burst.exceeded + ); +} + +export function useUsageLimitDetection() { + const billingEnabled = useFeatureFlag("posthog-code-billing"); + const { isPro } = useSeat(); + const { usage } = useUsage(); + const hasAlertedRef = useRef(false); + + useEffect(() => { + if (!billingEnabled || isPro || !usage) return; + + const exceeded = isExceeded(usage); + + if (exceeded && !hasAlertedRef.current) { + hasAlertedRef.current = true; + + const sessions = useSessionStore.getState().sessions; + const hasActiveSession = Object.values(sessions).some( + (s) => s.status === "connected" && s.isPromptPending, + ); + + useUsageLimitStore + .getState() + .show(hasActiveSession ? "mid-task" : "idle"); + } + + if (!exceeded) { + hasAlertedRef.current = false; + } + }, [billingEnabled, isPro, usage]); +} diff --git a/apps/code/src/renderer/features/billing/stores/usageLimitStore.ts b/apps/code/src/renderer/features/billing/stores/usageLimitStore.ts new file mode 100644 index 000000000..90cd3609b --- /dev/null +++ b/apps/code/src/renderer/features/billing/stores/usageLimitStore.ts @@ -0,0 +1,23 @@ +import { create } from "zustand"; + +type UsageLimitContext = "mid-task" | "idle"; + +interface UsageLimitState { + isOpen: boolean; + context: UsageLimitContext | null; +} + +interface UsageLimitActions { + show: (context: UsageLimitContext) => void; + hide: () => void; +} + +type UsageLimitStore = UsageLimitState & UsageLimitActions; + +export const useUsageLimitStore = create()((set) => ({ + isOpen: false, + context: null, + + show: (context) => set({ isOpen: true, context }), + hide: () => set({ isOpen: false, context: null }), +})); diff --git a/apps/code/src/renderer/features/settings/components/sections/PlanUsageSettings.tsx b/apps/code/src/renderer/features/settings/components/sections/PlanUsageSettings.tsx index 438dfe384..934684760 100644 --- a/apps/code/src/renderer/features/settings/components/sections/PlanUsageSettings.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/PlanUsageSettings.tsx @@ -1,3 +1,4 @@ +import { useUsage } from "@features/billing/hooks/useUsage"; import { useSeatStore } from "@features/billing/stores/seatStore"; import { useSeat } from "@hooks/useSeat"; import { @@ -16,8 +17,6 @@ import { Text, } from "@radix-ui/themes"; import { Tooltip } from "@renderer/components/ui/Tooltip"; -import { useTRPC } from "@renderer/trpc"; -import { useQuery } from "@tanstack/react-query"; import { getPostHogUrl } from "@utils/urls"; import { useState } from "react"; @@ -38,14 +37,6 @@ function formatResetTime(seconds: number): string { return `${days} days`; } -function useUsage() { - const trpc = useTRPC(); - const { data: usage, isLoading } = useQuery( - trpc.llmGateway.usage.queryOptions(), - ); - return { usage: usage ?? null, isLoading }; -} - export function PlanUsageSettings() { const { seat, diff --git a/apps/code/src/renderer/features/sidebar/components/SidebarContent.tsx b/apps/code/src/renderer/features/sidebar/components/SidebarContent.tsx index 42cabf8c5..eecb48fa6 100644 --- a/apps/code/src/renderer/features/sidebar/components/SidebarContent.tsx +++ b/apps/code/src/renderer/features/sidebar/components/SidebarContent.tsx @@ -1,4 +1,6 @@ import { useArchivedTaskIds } from "@features/archive/hooks/useArchivedTaskIds"; +import { SidebarUsageBar } from "@features/billing/components/SidebarUsageBar"; +import { useFeatureFlag } from "@hooks/useFeatureFlag"; import { ArchiveIcon } from "@phosphor-icons/react"; import { Box, Flex } from "@radix-ui/themes"; import { useNavigationStore } from "@stores/navigationStore"; @@ -12,6 +14,7 @@ export const SidebarContent: React.FC = () => { const navigateToArchived = useNavigationStore( (state) => state.navigateToArchived, ); + const billingEnabled = useFeatureFlag("posthog-code-billing"); return ( @@ -19,6 +22,7 @@ export const SidebarContent: React.FC = () => { + {billingEnabled && } {archivedTaskIds.size > 0 && ( - - - )} - {/* Local folder picker */} + {alternativeConnectedProject && selectedProject && ( + + + GitHub is already connected on{" "} + + {alternativeConnectedProject.name} + {" "} + ({alternativeConnectedProject.organization.name}). Switch to + that project, or click{" "} + Connect GitHub below to install a + new integration on{" "} + {selectedProject.name}. + + + + + + )} + {/* GitHub integration */}