From 06d71d69a182e3d97ab7475e6ef66b3f39ae110a Mon Sep 17 00:00:00 2001 From: iamjr15 Date: Wed, 29 Jul 2026 16:21:44 +0530 Subject: [PATCH] refactor(web): composer dedup, shared dismissal, query keys, file splits --- apps/web/src/app/(app)/101/page.tsx | 11 +- apps/web/src/app/(app)/models/page.tsx | 9 +- .../src/app/(app)/personalization/page.tsx | 9 +- apps/web/src/app/(app)/pricing/page.tsx | 9 +- apps/web/src/app/(app)/skills/page.tsx | 13 +- apps/web/src/app/(app)/usage/page.tsx | 9 +- apps/web/src/app/layout.tsx | 16 +- apps/web/src/app/providers.tsx | 32 +- .../billing/manage-subscription-dialog.tsx | 315 +----------------- .../subscription-cancellation-form.tsx | 149 +++++++++ .../src/components/billing/upgrade-dialog.tsx | 16 +- .../billing/use-manage-subscription.ts | 163 +++++++++ .../chat/chat-context-controller.ts | 33 +- .../components/chat/chat-panel-submission.ts | 13 +- .../components/chat/message-activity-model.ts | 278 ++++++++++++++++ .../src/components/chat/message-activity.tsx | 289 +--------------- .../components/chat/message-deliverables.tsx | 8 +- .../web/src/components/chat/message-parts.tsx | 2 +- .../src/components/chat/message-timeline.ts | 2 +- .../chat/prompt-composer-controller.ts | 154 +++------ .../components/chat/prompt-composer-view.tsx | 21 +- .../chat/use-chat-panel-controller.ts | 25 +- .../composer/add-menu-controller.ts | 35 +- .../components/composer/composer-textarea.tsx | 51 +++ .../composer/model-menu-controller.ts | 29 +- .../composer/project-picker-controller.ts | 51 +-- .../composer/project-picker-dialogs.tsx | 173 ++++++++++ .../composer/project-picker-parts.tsx | 260 +-------------- .../components/composer/project-picker.tsx | 2 +- .../components/composer/use-composer-menu.ts | 103 ++++++ .../composer/use-project-file-uploads.ts | 3 +- .../home/home-composer-plan-banner.tsx | 6 +- .../web/src/components/home/home-composer.tsx | 23 +- .../src/components/home/home-workspace.tsx | 36 +- .../home/use-home-composer-controller.ts | 98 +----- .../preview/browser-takeover-surface.tsx | 38 +++ .../preview/computer-panel-tabs.tsx | 27 +- .../preview/console-strip-header.tsx | 4 +- .../src/components/preview/console-strip.tsx | 4 +- .../preview/console-terminal-format.ts | 4 +- .../preview/console-terminal-pane.tsx | 8 +- .../preview/console-terminal-state.ts | 2 +- .../preview/console-terminal.types.ts | 2 +- .../components/preview/expo-device-panel.tsx | 111 ++++++ .../preview/preview-device-menu.tsx | 30 +- .../components/preview/preview-path-input.tsx | 2 +- .../preview/preview-session.tsx | 4 +- .../components/preview/preview-side-panel.tsx | 160 +-------- .../preview/preview-url-controls.tsx | 2 +- .../components/preview/sandbox-ide-tab.tsx | 5 +- .../{lib => components}/preview/url-bar.ts | 4 +- .../preview/use-console-terminal-data.ts | 12 +- .../preview/use-console-terminal.ts | 6 +- .../preview/use-ensure-preview-live.ts | 2 +- .../preview/use-preview-console.ts | 2 +- .../components/projects/projects-shell.tsx | 11 +- .../settings/activity-year-chart.tsx | 8 +- .../src/components/settings/pricing-panel.tsx | 1 - .../settings/sandbox-hours-meter-body.tsx | 15 +- .../settings/sandbox-hours-meter.tsx | 6 +- .../settings/settings-page-shell.tsx | 25 ++ .../src/components/settings/usage-panel.tsx | 6 +- apps/web/src/components/shell/app-chrome.tsx | 26 +- apps/web/src/components/shell/app-sidebar.tsx | 1 - .../components/shell/sidebar-chat-list.tsx | 5 +- .../components/shell/sidebar-controller.ts | 11 +- apps/web/src/components/shell/sidebar-data.ts | 11 +- .../shell/sidebar-expanded-account.tsx | 5 +- .../shell/sidebar-expanded-content.tsx | 1 - .../shell/sidebar-list-controls.tsx | 19 +- .../src/components/shell/sidebar-panel.tsx | 1 - .../components/shell/sidebar-project-list.tsx | 9 +- .../shell/sidebar-rail-more-menu.tsx | 27 +- .../web/src/components/shell/sidebar.types.ts | 1 - .../shell/use-auto-collapse-sidebar.ts | 29 ++ .../skills/integration-skills-catalog.tsx | 2 +- .../skills/integration-skills-controls.tsx | 16 +- .../skills/use-integration-skills-catalog.ts | 1 - .../components/skills/user-skills-section.tsx | 16 +- apps/web/src/lib/api/query-keys.ts | 29 ++ apps/web/src/lib/ui/roving-menu-focus.ts | 64 ++++ apps/web/src/lib/ui/use-dismissable.ts | 59 ++++ 82 files changed, 1614 insertions(+), 1666 deletions(-) create mode 100644 apps/web/src/components/billing/subscription-cancellation-form.tsx create mode 100644 apps/web/src/components/billing/use-manage-subscription.ts create mode 100644 apps/web/src/components/chat/message-activity-model.ts create mode 100644 apps/web/src/components/composer/composer-textarea.tsx create mode 100644 apps/web/src/components/composer/project-picker-dialogs.tsx create mode 100644 apps/web/src/components/composer/use-composer-menu.ts create mode 100644 apps/web/src/components/preview/browser-takeover-surface.tsx rename apps/web/src/{lib => components}/preview/console-terminal-format.ts (86%) rename apps/web/src/{lib => components}/preview/console-terminal-state.ts (98%) rename apps/web/src/{lib => components}/preview/console-terminal.types.ts (94%) create mode 100644 apps/web/src/components/preview/expo-device-panel.tsx rename apps/web/src/{lib => components}/preview/preview-session.tsx (98%) rename apps/web/src/{lib => components}/preview/url-bar.ts (98%) rename apps/web/src/{lib => components}/preview/use-console-terminal-data.ts (94%) rename apps/web/src/{lib => components}/preview/use-console-terminal.ts (96%) rename apps/web/src/{lib => components}/preview/use-ensure-preview-live.ts (99%) rename apps/web/src/{lib => components}/preview/use-preview-console.ts (97%) create mode 100644 apps/web/src/components/settings/settings-page-shell.tsx create mode 100644 apps/web/src/components/shell/use-auto-collapse-sidebar.ts create mode 100644 apps/web/src/lib/api/query-keys.ts create mode 100644 apps/web/src/lib/ui/roving-menu-focus.ts create mode 100644 apps/web/src/lib/ui/use-dismissable.ts diff --git a/apps/web/src/app/(app)/101/page.tsx b/apps/web/src/app/(app)/101/page.tsx index d92f10d4..d2a96355 100644 --- a/apps/web/src/app/(app)/101/page.tsx +++ b/apps/web/src/app/(app)/101/page.tsx @@ -1,15 +1,16 @@ import { Cheatcode101SectionView, Cheatcode101Toc } from "@/components/docs/cheatcode-101-sections"; +import { SettingsPageShell } from "@/components/settings/settings-page-shell"; import { CheatcodeMark } from "@/components/ui/cheatcode-mark"; import { CHEATCODE_101_SECTIONS, CHEATCODE_101_TAGLINE } from "@/content/cheatcode-101"; export default function Cheatcode101Page() { return ( -
-
-
+ ); } diff --git a/apps/web/src/app/(app)/models/page.tsx b/apps/web/src/app/(app)/models/page.tsx index 84dc6f76..7e90a48b 100644 --- a/apps/web/src/app/(app)/models/page.tsx +++ b/apps/web/src/app/(app)/models/page.tsx @@ -1,11 +1,10 @@ import { ModelsPanel } from "@/components/settings/models-panel"; +import { SettingsPageShell } from "@/components/settings/settings-page-shell"; export default function ModelsPage() { return ( -
-
- -
-
+ + + ); } diff --git a/apps/web/src/app/(app)/personalization/page.tsx b/apps/web/src/app/(app)/personalization/page.tsx index 1556a66c..22408c97 100644 --- a/apps/web/src/app/(app)/personalization/page.tsx +++ b/apps/web/src/app/(app)/personalization/page.tsx @@ -1,11 +1,10 @@ import { PersonalizationPanel } from "@/components/settings/personalization-panel"; +import { SettingsPageShell } from "@/components/settings/settings-page-shell"; export default function PersonalizationPage() { return ( -
-
- -
-
+ + + ); } diff --git a/apps/web/src/app/(app)/pricing/page.tsx b/apps/web/src/app/(app)/pricing/page.tsx index 3388c1bc..60348681 100644 --- a/apps/web/src/app/(app)/pricing/page.tsx +++ b/apps/web/src/app/(app)/pricing/page.tsx @@ -1,11 +1,10 @@ import { PricingPanel } from "@/components/settings/pricing-panel"; +import { SettingsPageShell } from "@/components/settings/settings-page-shell"; export default function PricingPage() { return ( -
-
- -
-
+ + + ); } diff --git a/apps/web/src/app/(app)/skills/page.tsx b/apps/web/src/app/(app)/skills/page.tsx index 279101f8..ccc48f06 100644 --- a/apps/web/src/app/(app)/skills/page.tsx +++ b/apps/web/src/app/(app)/skills/page.tsx @@ -1,15 +1,14 @@ import { Suspense } from "react"; +import { SettingsPageShell } from "@/components/settings/settings-page-shell"; import { IntegrationSkillsCatalog } from "@/components/skills/integration-skills-catalog"; import { CheatcodeLoader } from "@/components/ui/cheatcode-loader"; export default function SkillsPage() { return ( -
-
- }> - - -
-
+ + }> + + + ); } diff --git a/apps/web/src/app/(app)/usage/page.tsx b/apps/web/src/app/(app)/usage/page.tsx index 9b1d9549..6a2256b3 100644 --- a/apps/web/src/app/(app)/usage/page.tsx +++ b/apps/web/src/app/(app)/usage/page.tsx @@ -1,11 +1,10 @@ +import { SettingsPageShell } from "@/components/settings/settings-page-shell"; import { UsagePanel } from "@/components/settings/usage-panel"; export default function UsagePage() { return ( -
-
- -
-
+ + + ); } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 357183ae..5c0dbaa5 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,11 +1,12 @@ import { env } from "@cheatcode/env/web"; import { PRODUCTION_APP_ORIGIN } from "@cheatcode/env/web-config"; import { ClerkProvider } from "@clerk/nextjs"; +import { auth } from "@clerk/nextjs/server"; import { ui } from "@clerk/ui"; import { GeistMono } from "geist/font/mono"; import { GeistSans } from "geist/font/sans"; import type { Metadata } from "next"; -import type { ReactNode } from "react"; +import { type ReactNode, Suspense } from "react"; import "./globals.css"; import "./effects.css"; import { ClientObservability } from "@/components/observability/client-observability"; @@ -31,8 +32,9 @@ export const metadata: Metadata = { }, }; -export default async function RootLayout({ children }: { children: ReactNode }) { +export default function RootLayout({ children }: { children: ReactNode }) { const clerkPublishableKey = env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; + const sessionPromise = auth().then(({ orgId, userId }) => ({ orgId: orgId ?? null, userId })); return ( - - - {children} - + + + + {children} + + diff --git a/apps/web/src/app/providers.tsx b/apps/web/src/app/providers.tsx index 5d5ff650..9ff0f752 100644 --- a/apps/web/src/app/providers.tsx +++ b/apps/web/src/app/providers.tsx @@ -1,12 +1,11 @@ "use client"; -import { useAuth } from "@clerk/nextjs"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import dynamic from "next/dynamic"; import { ThemeProvider } from "next-themes"; import { NuqsAdapter } from "nuqs/adapters/next/app"; import type { ReactNode } from "react"; -import { useEffect, useLayoutEffect, useState } from "react"; +import { use, useEffect, useLayoutEffect, useState } from "react"; import { Toaster } from "sonner"; import { useAppStore } from "@/lib/store/app-store"; import { useChatTabsStore } from "@/lib/store/chat-tabs-store"; @@ -17,22 +16,39 @@ const CommandPalette = dynamic( { ssr: false }, ); -export function Providers({ children }: { children: ReactNode }) { +interface RootSession { + orgId: null | string; + userId: null | string; +} + +export function Providers({ + children, + sessionPromise, +}: { + children: ReactNode; + sessionPromise: Promise; +}) { return ( - {children} + {children} ); } -function IdentityQueryProvider({ children }: { children: ReactNode }) { - const { isLoaded, orgId, userId } = useAuth(); - const identity = isLoaded ? `${userId ?? "anonymous"}:${orgId ?? "personal"}` : "loading"; +function IdentityQueryProvider({ + children, + sessionPromise, +}: { + children: ReactNode; + sessionPromise: Promise; +}) { + const { orgId, userId } = use(sessionPromise); + const identity = `${userId ?? "anonymous"}:${orgId ?? "personal"}`; return ( - + {children} ); diff --git a/apps/web/src/components/billing/manage-subscription-dialog.tsx b/apps/web/src/components/billing/manage-subscription-dialog.tsx index 4c689a70..b53b208a 100644 --- a/apps/web/src/components/billing/manage-subscription-dialog.tsx +++ b/apps/web/src/components/billing/manage-subscription-dialog.tsx @@ -1,39 +1,19 @@ "use client"; -import type { - BillingCancel, - BillingCancellationReason, - BillingStateResponse, - BillingSubscriptionActionResponse, -} from "@cheatcode/types"; -import { type QueryClient, useMutation, useQueryClient } from "@tanstack/react-query"; -import { useState } from "react"; -import { toast } from "sonner"; -import { ChevronDown, CreditCard, Loader2, ModalShell } from "@/components/ui"; +import type { BillingStateResponse } from "@cheatcode/types"; +import { SubscriptionCancellationForm } from "@/components/billing/subscription-cancellation-form"; +import { + billingStatusLabel, + formatPeriodEnd, + type ManageSubscriptionController, + useManageSubscription, +} from "@/components/billing/use-manage-subscription"; +import { CreditCard, Loader2, ModalShell } from "@/components/ui"; import { CheatcodeLoader } from "@/components/ui/cheatcode-loader"; import { RecoveryCard } from "@/components/ui/recovery-card"; -import { - requestBillingCancellation, - requestBillingPortal, - requestBillingReactivation, -} from "@/lib/api/billing"; -import { BILLING_STATE_QUERY_KEY, useBillingStateQuery } from "@/lib/hooks/use-billing"; - -const CANCELLATION_REASON_LABELS: Record = { - customer_service: "Customer service", - low_quality: "Quality didn't meet my needs", - missing_features: "Missing features", - other: "Something else", - switched_service: "Switched to another service", - too_complex: "Too difficult to use", - too_expensive: "Too expensive", - unused: "I don't use it enough", -}; - -type DialogStep = "cancel" | "overview"; +import type { useBillingStateQuery } from "@/lib/hooks/use-billing"; interface ManageSubscriptionDialogProps { - getToken: () => Promise; onClose: () => void; open: boolean; planDisplayName: string; @@ -41,13 +21,12 @@ interface ManageSubscriptionDialogProps { } export function ManageSubscriptionDialog({ - getToken, onClose, open, planDisplayName, sandboxHoursTotal, }: ManageSubscriptionDialogProps) { - const controller = useManageSubscriptionController({ getToken, onClose, open, planDisplayName }); + const controller = useManageSubscription({ onClose, open, planDisplayName }); return ( ) { - const queryClient = useQueryClient(); - const stateQuery = useBillingStateQuery(getToken, open); - const [step, setStep] = useState("overview"); - const [reason, setReason] = useState(""); - const [comment, setComment] = useState(""); - const cancelMutation = useCancellationMutation(getToken, queryClient, () => setStep("overview")); - const portalMutation = useBillingPortalMutation(getToken); - const reactivateMutation = useReactivationMutation(getToken, queryClient, planDisplayName); - const isBusy = - cancelMutation.isPending || portalMutation.isPending || reactivateMutation.isPending; - - function closeDialog() { - if (isBusy) return; - setStep("overview"); - setReason(""); - setComment(""); - onClose(); - } - - function confirmCancellation() { - const trimmedComment = comment.trim(); - cancelMutation.mutate({ - ...(trimmedComment ? { comment: trimmedComment } : {}), - ...(reason ? { reason } : {}), - }); - } - - return { - closeDialog, - comment, - confirmCancellation, - isBusy, - openBillingPortal: () => portalMutation.mutate(), - reactivate: () => reactivateMutation.mutate(), - reason, - setComment, - setReason, - setStep, - stateQuery, - step, - }; -} - -function useCancellationMutation( - getToken: () => Promise, - queryClient: QueryClient, - onSuccess: () => void, -) { - return useMutation({ - mutationFn: (input: BillingCancel) => requestBillingCancellation(getToken, input), - onError: (error) => - toast.error(error instanceof Error ? error.message : "Plan cancellation failed"), - onSuccess: (result) => { - updateCachedBillingState(queryClient, result); - onSuccess(); - toast.success(cancellationSuccessMessage(result.currentPeriodEnd)); - }, - }); -} - -function useReactivationMutation( - getToken: () => Promise, - queryClient: QueryClient, - planDisplayName: string, -) { - return useMutation({ - mutationFn: () => requestBillingReactivation(getToken), - onError: (error) => - toast.error(error instanceof Error ? error.message : "Plan reactivation failed"), - onSuccess: (result) => { - updateCachedBillingState(queryClient, result); - toast.success(`${planDisplayName} will keep renewing`); - }, - }); -} - -function useBillingPortalMutation(getToken: () => Promise) { - return useMutation({ - mutationFn: () => requestBillingPortal(getToken), - onError: (error) => - toast.error(error instanceof Error ? error.message : "Billing portal couldn't open"), - onSuccess: (url) => window.location.assign(url), - }); -} - function ManageDialogFrame({ controller, planDisplayName, sandboxHoursTotal, }: { - controller: ReturnType; + controller: ManageSubscriptionController; planDisplayName: string; sandboxHoursTotal: number; }) { @@ -186,7 +74,7 @@ function ManageDialogBody({ planDisplayName, sandboxHoursTotal, }: { - controller: ReturnType; + controller: ManageSubscriptionController; planDisplayName: string; sandboxHoursTotal: number; }) { @@ -196,7 +84,7 @@ function ManageDialogBody({ if (!stateQuery.data) return null; if (controller.step === "cancel") { return ( - controller.setStep("overview")} @@ -424,181 +312,6 @@ function PlanManagementMessage({ ); } -function CancellationForm({ - comment, - isBusy, - onBack, - onCommentChange, - onConfirm, - onReasonChange, - periodEnd, - planDisplayName, - reason, -}: { - comment: string; - isBusy: boolean; - onBack: () => void; - onCommentChange: (value: string) => void; - onConfirm: () => void; - onReasonChange: (value: BillingCancellationReason | "") => void; - periodEnd: string | null; - planDisplayName: string; - reason: BillingCancellationReason | ""; -}) { - return ( -
-

Cancel {planDisplayName}?

-

- Your plan stays active until {formatPeriodEnd(periodEnd)}. You won't be charged again after - that date. -

-
- - -
- -
- ); -} - -function CancellationReasonField({ - disabled, - onChange, - value, -}: { - disabled: boolean; - onChange: (value: BillingCancellationReason | "") => void; - value: BillingCancellationReason | ""; -}) { - return ( - - ); -} - -function CancellationCommentField({ - disabled, - onChange, - value, -}: { - disabled: boolean; - onChange: (value: string) => void; - value: string; -}) { - return ( -