From 70efc8a95b0339f7b380de622c0e782a3f0c27d7 Mon Sep 17 00:00:00 2001 From: Nicholas Kissel Date: Thu, 28 May 2026 20:09:12 -0700 Subject: [PATCH 1/2] feat(frontend): agentOS onboarding template and create flow behind feature flag --- frontend/src/app/actors-grid.tsx | 96 ++++-- frontend/src/app/forms/stepper-form.tsx | 40 ++- frontend/src/app/getting-started.tsx | 321 ++++++++++++++++-- .../actors/dialogs/create-actor-sheet.tsx | 23 +- frontend/src/lib/features.ts | 2 + .../src/routes/_context/ns.$namespace.tsx | 15 + .../projects.$project/ns.$namespace.tsx | 15 + 7 files changed, 440 insertions(+), 72 deletions(-) diff --git a/frontend/src/app/actors-grid.tsx b/frontend/src/app/actors-grid.tsx index 73333ee2c2..cabfa44383 100644 --- a/frontend/src/app/actors-grid.tsx +++ b/frontend/src/app/actors-grid.tsx @@ -1,4 +1,4 @@ -import { faGear, faLogs, faPlus, Icon } from "@rivet-gg/icons"; +import { faChevronDown, faGear, faLogs, faPlus, Icon } from "@rivet-gg/icons"; import { queryOptions, useInfiniteQuery, @@ -17,6 +17,13 @@ import { SmallText, WithTooltip, } from "@/components"; +import { Badge } from "@/components/ui/badge"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { cloudEnv } from "@/lib/env"; import { features } from "@/lib/features"; import { ActorIcon } from "@/components/lazy-icon"; @@ -54,6 +61,57 @@ function GridCard({ ); } +// Header create affordance. With the agentOS feature flag on, the single +// "Create Actor" button becomes a "Create" menu offering Actor or agentOS; +// both open the same create dialog, the latter with agentOS-tailored copy. +function CreateMenu({ buttonVariant }: { buttonVariant: "outline" | "default" }) { + const navigate = useNavigate(); + const openModal = (modal: string) => + navigate({ to: ".", search: (old) => ({ ...old, modal }) }); + + if (!features.agentOs) { + return ( + + ); + } + + return ( + + + + + + openModal("create-actor")}> + Actor + + openModal("create-agent-os")}> + agentOS + + Preview + + + + + ); +} + function ActorGridCardSkeleton() { return (
@@ -128,7 +186,7 @@ export function ActorsGrid({ size="icon-sm" aria-label="Namespace settings" onClick={() => { - navigate({ + void navigate({ to: ".", search: (old) => ({ ...old, @@ -150,22 +208,7 @@ export function ActorsGrid({ Actors {builds.length > 0 ? ( - + ) : null} @@ -188,22 +231,7 @@ export function ActorsGrid({ Deploy code that registers an actor to see it here. - +
) ) : ( diff --git a/frontend/src/app/forms/stepper-form.tsx b/frontend/src/app/forms/stepper-form.tsx index 3493578e99..057730c2c5 100644 --- a/frontend/src/app/forms/stepper-form.tsx +++ b/frontend/src/app/forms/stepper-form.tsx @@ -35,7 +35,10 @@ export type StepConfirm> = ( type Step = Stepperize.Step & { assist?: boolean; - description?: string; + // Title/description may be static or derived from the live form values so a + // step can adapt its heading to earlier choices (e.g. a template selection). + description?: string | ((values: Record) => string); + titleFor?: (values: Record) => string; schema: z.ZodSchema | ((values: Record) => z.ZodSchema); next?: string; previous?: string; @@ -195,6 +198,30 @@ function AnimatedHeight({ children }: { children: ReactNode }) { ); } +// Resolves a step's title/description against the live form values so headings +// can adapt to earlier choices. Subscribes via useWatch so it re-renders when +// those values change. +function StepHeading({ step }: { step: Step }) { + const values = useWatch() as Record; + const title = step.titleFor ? step.titleFor(values) : step.title; + const description = + typeof step.description === "function" + ? step.description(values) + : step.description; + return ( + <> +
+

{title}

+
+ {description ? ( +

+ {description} +

+ ) : null} + + ); +} + function getNextVisibleStepId( steps: Step[], currentId: string, @@ -364,16 +391,7 @@ function Content({ ease: [0.4, 0, 0.2, 1], }} > -
-

- {step.title} -

-
- {(step as Step).description ? ( -

- {(step as Step).description} -

- ) : null} +
Stepper={Stepper} diff --git a/frontend/src/app/getting-started.tsx b/frontend/src/app/getting-started.tsx index 16ee41fc46..5960e3c4a8 100644 --- a/frontend/src/app/getting-started.tsx +++ b/frontend/src/app/getting-started.tsx @@ -1,8 +1,10 @@ import { Accordion, AccordionContent } from "@radix-ui/react-accordion"; import { + faActors, faArrowRight, faCheck, faCopy, + faKey, faSpinnerThird, Icon, } from "@rivet-gg/icons"; @@ -76,8 +78,14 @@ const stepper = defineStepper( { id: "run", title: "Build your first Actor", - description: - "Let your coding agent scaffold an Actor, or follow the quickstart yourself.", + titleFor: (values) => + values.template === "agent-os" + ? "Set up agentOS" + : "Build your first Actor", + description: (values) => + values.template === "agent-os" + ? "Boot an agentOS instance and run your first session, locally." + : "Let your coding agent scaffold an Actor, or follow the quickstart yourself.", next: "Continue", schema: z.object({}), group: "local", @@ -215,6 +223,7 @@ export function GettingStarted({ datacenters: {}, datacenter: "", mode: "serverless" as "serverless" | "serverfull", + template: "actor" as "actor" | "agent-os", ...(initialRunnerConfig || {}), }; @@ -621,6 +630,36 @@ function ProviderCard({ ); } +function AgentOsKeyNotice() { + return ( +
+ +
+

agentOS needs an LLM key

+

+ Set{" "} + + ANTHROPIC_API_KEY + {" "} + as a secret on your deployment. The VM doesn't inherit it + from the host, so your server passes it to each session.{" "} + + Learn more + +

+
+
+ ); +} + function OrDivider({ label }: { label: string }) { return (
@@ -653,9 +692,139 @@ function CommandBox({ command }: { command: string }) { ); } +// agentOS brand mark (rounded square + "OS") drawn in currentColor so it adapts +// to the theme, unlike the white-only marketing SVG. +function AgentOsLogo({ className }: { className?: string }) { + return ( + + ); +} + +function BuildTargetCard({ + icon, + label, + description, + badge, + isSelected, + onSelect, +}: { + icon: ReactNode; + label: string; + description: string; + badge?: string; + isSelected: boolean; + onSelect: () => void; +}) { + return ( + + ); +} + +function BuildTargetSelector() { + const { control, setValue } = useFormContext(); + return ( + ( +
+

What are you building?

+
+ + } + label="Rivet Actors" + description="Realtime, state, and multiplayer for any app" + isSelected={field.value !== "agent-os"} + onSelect={() => + setValue("template", "actor", { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }) + } + /> + } + label="agentOS" + badge="Preview" + description="Sandboxed VMs with a filesystem, network, and processes" + isSelected={field.value === "agent-os"} + onSelect={() => + setValue("template", "agent-os", { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }) + } + /> +
+
+ )} + /> + ); +} + +const AGENT_OS_PACKAGES = + "rivetkit @rivet-dev/agent-os-common @rivet-dev/agent-os-pi"; + function InstallStep() { + const isAgentOs = useWatch({ name: "template" }) === "agent-os"; return (
+ {features.agentOs ? : null}
@@ -672,18 +841,33 @@ function InstallStep() {
-

Add the RivetKit package

+

+ {isAgentOs + ? "Add the agentOS packages" + : "Add the RivetKit package"} +

- Install the library your app imports to define and call - Actors. + {isAgentOs + ? "Install RivetKit plus the agentOS runtime and the Pi agent." + : "Install the library your app imports to define and call Actors."}

- + {isAgentOs ? ( + + ) : ( + + )}
@@ -804,12 +988,82 @@ For detailed setup instructions, see the quickstart guides: Check the troubleshooting guide at https://rivet.dev/docs/actors/troubleshooting. If that doesn't help, prompt the user to join the Rivet Discord (https://rivet.dev/discord) or file an issue on GitHub (https://github.com/rivet-dev/rivet). Generate a report with: symptoms (error, local vs deployed), what you've tried, and environment (RivetKit version, runtime, provider, HTTP router).`; +const agentOsPrompt = `# agentOS Setup + +I want to add agentOS to this project using RivetKit. agentOS gives agents and sandboxed code their own isolated VMs with a full filesystem, shell, network, and persistent state. Pi is one coding agent you can run inside it, and this setup uses Pi as the example. An agentOS actor is a normal Rivet Actor, so it deploys the same way as any other actor. + +Read https://rivet.dev/docs/agent-os/quickstart before making changes. agentOS is in preview. + +## Steps + +### Step 1: Install + +\`\`\`bash +npm install rivetkit @rivet-dev/agent-os-common @rivet-dev/agent-os-pi +\`\`\` + +### Step 2: Create the server + +\`\`\`ts +import { agentOs } from "rivetkit/agent-os"; +import { setup } from "rivetkit"; +import common from "@rivet-dev/agent-os-common"; +import pi from "@rivet-dev/agent-os-pi"; + +const vm = agentOs({ options: { software: [common, pi] } }); + +export const registry = setup({ use: { vm } }); +registry.start(); +\`\`\` + +### Step 3: Configure the model key + +The agent needs an LLM key at runtime. Set \`ANTHROPIC_API_KEY\` in the environment locally, and once deployed, set it as a deployment secret. Never hardcode it. + +### Step 4: Boot an instance and run a prompt + +\`\`\`ts +import { createClient } from "rivetkit/client"; +import type { registry } from "./server"; + +const client = createClient(); + +// getOrCreate boots the agentOS instance (the VM) on first call. +const agent = client.vm.getOrCreate(["my-agent"]); + +agent.on("sessionEvent", (data) => console.log(data.event)); + +const session = await agent.createSession("pi", { + env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! }, +}); +await agent.sendPrompt( + session.sessionId, + "Write a hello world script to /home/user/hello.js", +); + +const content = await agent.readFile("/home/user/hello.js"); +console.log(new TextDecoder().decode(content)); +\`\`\` + +### Step 5: Verify + +Run the server, then the client, and confirm the agent created the file. Then verify it works through the Rivet inspector or your deployed endpoint. + +## If You Get Stuck + +agentOS is in preview. See https://rivet.dev/docs/agent-os, the troubleshooting guide at https://rivet.dev/docs/actors/troubleshooting, or the Rivet Discord (https://rivet.dev/discord).`; + function RunLocallyStep() { + const isAgentOs = useWatch({ name: "template" }) === "agent-os"; return (
@@ -818,12 +1072,18 @@ function RunLocallyStep() { Follow the quickstart guide

- Build a Rivet Actor project by hand, step by step. + {isAgentOs + ? "Build an agentOS agent by hand, step by step." + : "Build a Rivet Actor project by hand, step by step."}

+ ); +} diff --git a/frontend/src/components/onboarding/agent-os/software-select-step.tsx b/frontend/src/components/onboarding/agent-os/software-select-step.tsx new file mode 100644 index 0000000000..f7b1a3eca4 --- /dev/null +++ b/frontend/src/components/onboarding/agent-os/software-select-step.tsx @@ -0,0 +1,52 @@ +import { useController } from "react-hook-form"; +import { DEFAULT_PACKAGES, SOFTWARE } from "./catalog"; +import { SelectCard } from "./select-card"; + +// Presentational multi-select for the software packages baked into the build. +export function SoftwareSelect({ + value, + onChange, +}: { + value: string[]; + onChange: (slugs: string[]) => void; +}) { + const toggle = (slug: string) => { + onChange( + value.includes(slug) + ? value.filter((s) => s !== slug) + : [...value, slug], + ); + }; + + return ( +
+

+ Packages are baked into the build image and are immutable after + deploy, so choose what your agent needs now. +

+
+ {SOFTWARE.map((pkg) => ( + toggle(pkg.slug)} + /> + ))} +
+
+ ); +} + +// Wizard step: binds SoftwareSelect to the react-hook-form `packages` field. +export function SoftwareSelectStep() { + const { field } = useController({ name: "packages" }); + return ( + + ); +}