From 8a5e17956a191d9bc4670f4d6ae3a1ae96808845 Mon Sep 17 00:00:00 2001 From: ABCxFF <79597906+abcxff@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:14:42 +0000 Subject: [PATCH] feat(frontend): sync compute connect dialog with onboarding and simplify others --- frontend/src/app/compute-deploy.tsx | 166 ++++++++++++++++++ .../src/app/dialogs/connect-aws-frame.tsx | 4 +- .../src/app/dialogs/connect-hetzner-frame.tsx | 4 +- .../connect-manual-serverless-frame.tsx | 21 ++- .../src/app/dialogs/connect-rivet-frame.tsx | 62 +++---- frontend/src/app/getting-started.tsx | 159 ++--------------- frontend/src/app/provider-dropdown.tsx | 3 - frontend/src/content/agent-prompts.ts | 11 +- 8 files changed, 229 insertions(+), 201 deletions(-) create mode 100644 frontend/src/app/compute-deploy.tsx diff --git a/frontend/src/app/compute-deploy.tsx b/frontend/src/app/compute-deploy.tsx new file mode 100644 index 0000000000..45a89bec0b --- /dev/null +++ b/frontend/src/app/compute-deploy.tsx @@ -0,0 +1,166 @@ +import { faCopy, Icon } from "@rivet-gg/icons"; +import { deployOptions, type Provider } from "@rivetkit/shared-data"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { Badge, Button, CodeFrame, CodePreview } from "@/components"; +import { + useCloudNamespaceDataProvider, + useEngineCompatDataProvider, +} from "@/components/actors"; +import { + getAgentInstructionsPrompt, + getComputeAddendum, +} from "@/content/agent-prompts"; +import { cloudEnv, getRivetRunUrl } from "@/lib/env"; +import { usePublishableToken } from "@/queries/accessors"; +import { useRivetDsn } from "./env-variables"; + +// Providers that can only run serverless (function) deployments. Every other +// platform defaults to a long-lived runner (serverful) but can still toggle to +// serverless. +const SERVERLESS_ONLY_PROVIDERS = new Set([ + "vercel", + "cloudflare-workers", + "supabase-functions", + "gcp-cloud-run", +]); + +export function isServerlessOnlyProvider(provider: unknown): boolean { + return SERVERLESS_ONLY_PROVIDERS.has(provider as Provider); +} + +export function defaultRuntimeModeForProvider( + provider: unknown, +): "serverless" | "serverful" { + return isServerlessOnlyProvider(provider) ? "serverless" : "serverful"; +} + +export function useAgentInstructionsCode({ + provider, + runnerName = "default", + endpoint, + mode, +}: { + provider?: Provider; + runnerName?: string; + endpoint?: string; + mode?: "serverless" | "serverful"; +} = {}) { + const providerDetails = provider + ? deployOptions.find((p) => p.name === provider) + : undefined; + const providerStr = + providerDetails?.displayName ?? provider ?? "your chosen provider"; + const providerDocUrl = providerDetails?.href + ? `https://rivet.dev${providerDetails.href}` + : undefined; + // Follow the user's runner/serverless selection. Rivet Compute always + // deploys serverless (it sets the mode on deploy); every other provider uses + // the chosen mode, falling back to the provider's default. + const serverless = + provider === "rivet" || + (mode ?? defaultRuntimeModeForProvider(provider)) === "serverless"; + const publishableToken = useRivetDsn({ kind: "publishable", endpoint }); + const secretToken = useRivetDsn({ kind: "secret", endpoint }); + const namespace = useEngineCompatDataProvider().engineNamespace; + + return getAgentInstructionsPrompt({ + providerStr, + publishableToken, + secretToken, + runnerName, + serverless, + providerDocUrl, + namespace, + }); +} + +// Builds the Rivet Compute copy-prompt (generic instructions + compute addendum) +// and exposes the cloud token and namespace so callers can also render a manual +// `@rivetkit/cli deploy` command. +export function useComputeInstructionsCode() { + const agentInstructions = useAgentInstructionsCode({ provider: "rivet" }); + const dataProvider = useCloudNamespaceDataProvider(); + const { data: cloudToken } = useSuspenseQuery( + dataProvider.createApiTokenQueryOptions({ name: "Onboarding" }), + ); + const publishableRawToken = usePublishableToken(); + const namespace = dataProvider.engineNamespace; + + const computeAddendum = getComputeAddendum({ + cloudToken, + publishableToken: publishableRawToken ?? "", + namespace, + apiUrl: cloudEnv().VITE_APP_API_URL, + cloudApiUrl: cloudEnv().VITE_APP_CLOUD_API_URL, + rivetRunUrl: getRivetRunUrl(namespace), + }); + + return { + code: `${agentInstructions}\n\n---\n\n${computeAddendum}`, + cloudToken, + namespace, + }; +} + +export function CommandBox({ command }: { command: string }) { + return ( + command} + hideFooter + className="group my-0" + > + + + ); +} + +// Compact "Copy prompt" button that copies the agent prompt to the clipboard +// instead of dumping the whole prompt inline. +export function AgentPromptBanner({ + code, + containsSecret = false, + title = "Use your coding agent", + description = "Have your coding agent complete these steps to deploy to Rivet Compute.", +}: { + code: string; + containsSecret?: boolean; + title?: string; + description?: string; +}) { + return ( + + + ); +} diff --git a/frontend/src/app/dialogs/connect-aws-frame.tsx b/frontend/src/app/dialogs/connect-aws-frame.tsx index 5d2144059d..31d4e9bffa 100644 --- a/frontend/src/app/dialogs/connect-aws-frame.tsx +++ b/frontend/src/app/dialogs/connect-aws-frame.tsx @@ -1,6 +1,6 @@ import { faAws, Icon } from "@rivet-gg/icons"; import { type DialogContentProps, Frame } from "@/components"; -import ConnectManualServerlessFrameContent from "./connect-manual-serverless-frame"; +import ConnectManualServerfulFrameContent from "./connect-manual-serverful-frame"; interface ConnectAwsFrameContentProps extends DialogContentProps { footer?: React.ReactNode; @@ -23,7 +23,7 @@ export default function ConnectAwsFrameContent({ - diff --git a/frontend/src/app/dialogs/connect-hetzner-frame.tsx b/frontend/src/app/dialogs/connect-hetzner-frame.tsx index 760e1dd308..74a5ad5586 100644 --- a/frontend/src/app/dialogs/connect-hetzner-frame.tsx +++ b/frontend/src/app/dialogs/connect-hetzner-frame.tsx @@ -1,6 +1,6 @@ import { faHetznerH, Icon } from "@rivet-gg/icons"; import { type DialogContentProps, Frame } from "@/components"; -import ConnectManualServerlessFrameContent from "./connect-manual-serverless-frame"; +import ConnectManualServerfulFrameContent from "./connect-manual-serverful-frame"; interface ConnectHetznerFrameContentProps extends DialogContentProps { title?: React.ReactNode; @@ -23,7 +23,7 @@ export default function ConnectHetznerFrameContent({ - diff --git a/frontend/src/app/dialogs/connect-manual-serverless-frame.tsx b/frontend/src/app/dialogs/connect-manual-serverless-frame.tsx index 391f6790ce..08d0a16b5c 100644 --- a/frontend/src/app/dialogs/connect-manual-serverless-frame.tsx +++ b/frontend/src/app/dialogs/connect-manual-serverless-frame.tsx @@ -1,5 +1,5 @@ import type { Rivet } from "@rivetkit/engine-api-full"; -import type { Provider } from "@rivetkit/shared-data"; +import { deployOptions, type Provider } from "@rivetkit/shared-data"; import { useMutation, usePrefetchInfiniteQuery, @@ -122,7 +122,7 @@ function FormStepper({ }} content={{ "step-1": () => , - "step-2": () => , + "step-2": () => , "step-3": () => , }} /> @@ -225,10 +225,23 @@ function Step1({ provider }: { provider: Provider }) { ); } -function Step2() { +function Step2({ provider }: { provider: Provider }) { + const providerOptions = deployOptions.find( + (option) => option.name === provider, + ); return ( <> -

Set the following environment variables.

+

+ + Follow the integration guide here + + , and make sure to set the following environment variables: +

- Copy this prompt into your coding agent + Deploy your project

- - {[ - - } - > - - , - ]} - + + } + > + +
@@ -131,25 +123,19 @@ function StepNumber({ n }: { n: number }) { } function AgentInstructions() { - const publishableToken = useRivetDsn({ kind: "publishable" }); - const secretToken = useRivetDsn({ kind: "secret" }); - - const code = getAgentInstructionsPrompt({ - providerStr: "your chosen provider", - publishableToken, - secretToken, - runnerName: "default", - serverless: true, - }); + const { code, cloudToken, namespace } = useComputeInstructionsCode(); + const deployCommand = `npx @rivetkit/cli deploy --token ${ + cloudToken ?? "" + } --namespace ${namespace}`; return ( - code} className="m-0"> - - +
+ +

+ Or deploy manually from your project root: +

+ +
); } diff --git a/frontend/src/app/getting-started.tsx b/frontend/src/app/getting-started.tsx index d09d24465a..15079f0f46 100644 --- a/frontend/src/app/getting-started.tsx +++ b/frontend/src/app/getting-started.tsx @@ -3,7 +3,6 @@ import { faArrowRight, faCheck, faChevronDown, - faCopy, faKey, Icon, } from "@rivet-gg/icons"; @@ -18,7 +17,6 @@ import { Link, useNavigate } from "@tanstack/react-router"; import { motion } from "framer-motion"; import { type ReactNode, Suspense, useContext, useMemo } from "react"; import { useFormContext, useWatch } from "react-hook-form"; -import { toast } from "sonner"; import { match } from "ts-pattern"; import z from "zod"; import * as ConnectServerfulForm from "@/app/forms/connect-manual-serverful-form"; @@ -37,9 +35,8 @@ import { } from "@/components/actors"; import { defineStepper } from "@/components/ui/stepper"; import { deriveProviderFromMetadata } from "@/lib/data"; -import { cloudEnv, engineEnv, getRivetRunUrl } from "@/lib/env"; +import { engineEnv } from "@/lib/env"; import { features } from "@/lib/features"; -import { usePublishableToken } from "@/queries/accessors"; import { queryClient } from "@/queries/global"; import { cn } from "../components/lib/utils"; import { Badge } from "../components/ui/badge"; @@ -58,7 +55,7 @@ import { Configuration, ConfigurationAccordion, } from "./dialogs/connect-manual-serverless-frame"; -import { EnvVariables, useRivetDsn } from "./env-variables"; +import { EnvVariables } from "./env-variables"; import { StepperForm, StepVisibilityContext } from "./forms/stepper-form"; import { Content } from "./layout"; import { AgentSelectStep } from "@/components/onboarding/agent-os/agent-select-step"; @@ -69,9 +66,12 @@ import { DEFAULT_SANDBOX_PROVIDER, } from "@/components/onboarding/agent-os/catalog"; import { - getAgentInstructionsPrompt, - getComputeAddendum, -} from "@/content/agent-prompts"; + AgentPromptBanner, + CommandBox, + defaultRuntimeModeForProvider, + useAgentInstructionsCode, + useComputeInstructionsCode, +} from "./compute-deploy"; function platformTitle(provider: unknown): string { return ( @@ -80,26 +80,6 @@ function platformTitle(provider: unknown): string { ); } -// Providers that can only run serverless (function) deployments. Every other -// platform defaults to a long-lived runner (serverful) but can still toggle to -// serverless. -const SERVERLESS_ONLY_PROVIDERS = new Set([ - "vercel", - "cloudflare-workers", - "supabase-functions", - "gcp-cloud-run", -]); - -function isServerlessOnlyProvider(provider: unknown): boolean { - return SERVERLESS_ONLY_PROVIDERS.has(provider as Provider); -} - -function defaultRuntimeModeForProvider( - provider: unknown, -): "serverless" | "serverful" { - return isServerlessOnlyProvider(provider) ? "serverless" : "serverful"; -} - const stepper = defineStepper( { id: "local", @@ -686,19 +666,6 @@ function OrDivider({ label }: { label: string }) { ); } -function CommandBox({ command }: { command: string }) { - return ( - command} - hideFooter - className="group my-0" - > - - - ); -} - function AgentOsKeyNotice() { return (
@@ -973,7 +940,7 @@ function AgentOsHandoff() { // compute deployment addendum alongside the onboarding instructions. Copying it // gives the agent everything it needs to scaffold, run, and deploy in one paste. function RunLocallyComputeBanner() { - const code = useComputeInstructionsCode(); + const { code } = useComputeInstructionsCode(); return ( p.name === provider) - : undefined; - const providerStr = - providerDetails?.displayName ?? provider ?? "your chosen provider"; - const providerDocUrl = providerDetails?.href - ? `https://rivet.dev${providerDetails.href}` - : undefined; - // Follow the user's runner/serverless selection. Rivet Compute always - // deploys serverless (it sets the mode on deploy); every other provider uses - // the chosen mode, falling back to the provider's default. - const serverless = - provider === "rivet" || - (mode ?? defaultRuntimeModeForProvider(provider)) === "serverless"; - const publishableToken = useRivetDsn({ kind: "publishable", endpoint }); - const secretToken = useRivetDsn({ kind: "secret", endpoint }); - - return getAgentInstructionsPrompt({ - providerStr, - publishableToken, - secretToken, - runnerName, - serverless, - providerDocUrl, - }); -} - -function useComputeInstructionsCode() { - const agentInstructions = useAgentInstructionsCode({ provider: "rivet" }); - const dataProvider = useCloudNamespaceDataProvider(); - const { data: cloudToken } = useSuspenseQuery( - dataProvider.createApiTokenQueryOptions({ name: "Onboarding" }), - ); - const publishableRawToken = usePublishableToken(); - const namespace = dataProvider.engineNamespace; - - const computeAddendum = getComputeAddendum({ - cloudToken, - publishableToken: publishableRawToken ?? "", - namespace, - apiUrl: cloudEnv().VITE_APP_API_URL, - cloudApiUrl: cloudEnv().VITE_APP_CLOUD_API_URL, - rivetRunUrl: getRivetRunUrl(namespace), - }); - - return `${agentInstructions}\n\n---\n\n${computeAddendum}`; -} - function CopyAgentInstructionsButton({ provider }: { provider?: Provider }) { // The compute prompt reads cloud-namespace data; only available with compute. if (provider === "rivet" && features.compute) { @@ -1070,55 +978,8 @@ function CopyAgentInstructionsButton({ provider }: { provider?: Provider }) { return ; } -function AgentPromptBanner({ - code, - containsSecret = false, - title = "Use your coding agent", - description = "Have your coding agent complete these steps to deploy to Rivet Compute.", -}: { - code: string; - containsSecret?: boolean; - title?: string; - description?: string; -}) { - return ( - - - ); -} - function ComputeCopyAgentInstructionsButton() { - const code = useComputeInstructionsCode(); + const { code } = useComputeInstructionsCode(); return ; } diff --git a/frontend/src/app/provider-dropdown.tsx b/frontend/src/app/provider-dropdown.tsx index 210afed7e7..15836e6f5a 100644 --- a/frontend/src/app/provider-dropdown.tsx +++ b/frontend/src/app/provider-dropdown.tsx @@ -138,9 +138,6 @@ function RivetCloudDropdownMenuItem() { } > Rivet Compute {config ? "(Connected)" : ""} - - Beta - ); } diff --git a/frontend/src/content/agent-prompts.ts b/frontend/src/content/agent-prompts.ts index 56026fa53b..0b117d9b16 100644 --- a/frontend/src/content/agent-prompts.ts +++ b/frontend/src/content/agent-prompts.ts @@ -77,10 +77,10 @@ Verify the container starts and is connectable (e.g. \`curl http://localhost:300 ## Step 4: Deploy with the Rivet CLI -Deploy the project with a single command. \`@rivetkit/cli\` builds the \`Dockerfile\`, pushes the image to Rivet's registry, and creates/updates the \`default\` managed pool. The project, organization, and namespace are auto-detected from the token, so you do not need to pass them: +Deploy the project with a single command. \`@rivetkit/cli\` builds the \`Dockerfile\`, pushes the image to Rivet's registry, and creates/updates the \`default\` managed pool. Always pass \`--namespace ${namespace}\` so the deploy targets this namespace and not the default \`production\` namespace. The project and organization are auto-detected from the token: \`\`\`bash -npx @rivetkit/cli deploy --token "${cloudToken}" --env PORT=3000 +npx @rivetkit/cli deploy --token "${cloudToken}" --namespace ${namespace} --env PORT=3000 \`\`\` Notes: @@ -150,6 +150,7 @@ export function getAgentInstructionsPrompt({ runnerName, serverless, providerDocUrl, + namespace, }: { providerStr: string; publishableToken: string; @@ -157,9 +158,13 @@ export function getAgentInstructionsPrompt({ runnerName: string; serverless: boolean; providerDocUrl?: string; + namespace?: string; }) { const poolLine = runnerName !== "default" ? `\n RIVET_POOL=${runnerName}` : ""; + const namespaceNote = namespace + ? `> **Important:** Run every step below against the \`${namespace}\` namespace only, and pass \`--namespace ${namespace}\` with the deploy command. Do not deploy to or modify any other namespace (for example the default \`production\` namespace).\n\n` + : ""; const docLine = providerDocUrl ? `Review the deploy guide for ${providerStr}: ${providerDocUrl}` : `Review the deploy guide for ${providerStr} at https://rivet.dev/docs/deploy/`; @@ -188,7 +193,7 @@ ${deployEnv} return `# RivetKit Setup & Deploy -Read https://rivet.dev/llms.txt to understand how RivetKit works (actors, state, events, actions, connections, clients). +${namespaceNote}Read https://rivet.dev/llms.txt to understand how RivetKit works (actors, state, events, actions, connections, clients). Add a note to the project's \`CLAUDE.md\` / \`AGENTS.md\` (create one if missing) linking to https://rivet.dev/llms.txt as the reference for future RivetKit work.