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..8722f86f2
--- /dev/null
+++ b/apps/code/src/renderer/features/billing/components/SidebarUsageBar.tsx
@@ -0,0 +1,46 @@
+import { useUsage } from "@features/billing/hooks/useUsage";
+import { isUsageExceeded } from "@features/billing/utils";
+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 { isPro } = useSeat();
+ const { usage } = useUsage({ enabled: !isPro });
+
+ if (isPro || !usage) return null;
+
+ const usagePercent = Math.max(
+ usage.sustained.used_percent,
+ usage.burst.used_percent,
+ );
+ const exceeded = isUsageExceeded(usage);
+
+ 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..4cd77bf0e
--- /dev/null
+++ b/apps/code/src/renderer/features/billing/hooks/useUsage.ts
@@ -0,0 +1,17 @@
+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({ enabled = true }: { enabled?: boolean } = {}) {
+ const trpc = useTRPC();
+ const focused = useRendererWindowFocusStore((s) => s.focused);
+ const { data: usage, isLoading } = useQuery({
+ ...trpc.llmGateway.usage.queryOptions(),
+ enabled,
+ refetchInterval: focused && enabled ? 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..4e70273ec
--- /dev/null
+++ b/apps/code/src/renderer/features/billing/hooks/useUsageLimitDetection.ts
@@ -0,0 +1,37 @@
+import { useUsageLimitStore } from "@features/billing/stores/usageLimitStore";
+import { isUsageExceeded } from "@features/billing/utils";
+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";
+
+export function useUsageLimitDetection() {
+ const billingEnabled = useFeatureFlag("posthog-code-billing");
+ const { isPro } = useSeat();
+ const { usage } = useUsage({ enabled: billingEnabled && !isPro });
+ const hasAlertedRef = useRef(false);
+
+ useEffect(() => {
+ if (!billingEnabled || isPro || !usage) return;
+
+ const exceeded = isUsageExceeded(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/billing/utils.ts b/apps/code/src/renderer/features/billing/utils.ts
new file mode 100644
index 000000000..9179d31b1
--- /dev/null
+++ b/apps/code/src/renderer/features/billing/utils.ts
@@ -0,0 +1,11 @@
+interface UsageLimitCheck {
+ sustained: { exceeded: boolean };
+ burst: { exceeded: boolean };
+ is_rate_limited: boolean;
+}
+
+export function isUsageExceeded(usage: UsageLimitCheck): boolean {
+ return (
+ usage.is_rate_limited || usage.sustained.exceeded || usage.burst.exceeded
+ );
+}
diff --git a/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx b/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx
index 13f31ae65..90c29043a 100644
--- a/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx
+++ b/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx
@@ -248,31 +248,6 @@ export function GitIntegrationStep({
- {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}.
-
-
-
-
-
- )}
-
{/* 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 */}
{
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 && (