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..c869f63deb 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"; @@ -16,7 +18,13 @@ import { } from "@tanstack/react-query"; import { Link, useNavigate, useParams, useRouter } from "@tanstack/react-router"; import { motion } from "framer-motion"; -import { type ReactNode, Suspense, useEffect, useMemo } from "react"; +import { + type ReactNode, + Suspense, + useContext, + useEffect, + useMemo, +} from "react"; import { useFormContext, useWatch } from "react-hook-form"; import { toast } from "sonner"; import { match } from "ts-pattern"; @@ -60,8 +68,17 @@ import { ConfigurationAccordion, } from "./dialogs/connect-manual-serverless-frame"; import { EnvVariables, useRivetDsn } from "./env-variables"; -import { StepperForm } from "./forms/stepper-form"; +import { StepperForm, StepVisibilityContext } from "./forms/stepper-form"; import { Content } from "./layout"; +import { AgentSelectStep } from "@/components/onboarding/agent-os/agent-select-step"; +import { SoftwareSelectStep } from "@/components/onboarding/agent-os/software-select-step"; +import { SandboxMountStep } from "@/components/onboarding/agent-os/sandbox-mount-step"; +import { buildAgentOsSetup } from "@/components/onboarding/agent-os/build-agent-os-setup"; +import { + DEFAULT_AGENT, + DEFAULT_PACKAGES, + DEFAULT_SANDBOX_PROVIDER, +} from "@/components/onboarding/agent-os/catalog"; const stepper = defineStepper( { @@ -73,11 +90,53 @@ const stepper = defineStepper( schema: z.object({}), group: "local", }, + { + id: "agent", + title: "Choose your agent", + description: "Pick the coding agent to run inside agentOS.", + next: "Continue", + schema: z.object({ agent: z.string().nonempty() }), + group: "local", + isVisible: (values: Record) => + values.template === "agent-os", + }, + { + id: "software", + title: "Choose software", + description: "Select the packages baked into your build image.", + next: "Continue", + schema: z.object({ packages: z.array(z.string()) }), + group: "local", + isVisible: (values: Record) => + values.template === "agent-os", + }, + { + id: "sandbox", + title: "Sandbox & mounts", + description: + "Optionally mount a full sandbox for heavy workloads. Off by default.", + next: "Continue", + schema: z.object({ + sandbox: z.object({ + enabled: z.boolean(), + provider: z.string().optional(), + }), + }), + group: "local", + isVisible: (values: Record) => + values.template === "agent-os", + }, { id: "run", title: "Build your first Actor", - description: - "Let your coding agent scaffold an Actor, or follow the quickstart yourself.", + titleFor: (values: Record) => + values.template === "agent-os" + ? "Set up agentOS" + : "Build your first Actor", + description: (values: Record) => + 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 +274,13 @@ export function GettingStarted({ datacenters: {}, datacenter: "", mode: "serverless" as "serverless" | "serverfull", + template: "actor" as "actor" | "agent-os", + agent: DEFAULT_AGENT, + packages: DEFAULT_PACKAGES, + sandbox: { enabled: false, provider: DEFAULT_SANDBOX_PROVIDER } as { + enabled: boolean; + provider?: string; + }, ...(initialRunnerConfig || {}), }; @@ -251,6 +317,21 @@ export function GettingStarted({ ), + agent: () => ( + + + + ), + software: () => ( + + + + ), + sandbox: () => ( + + + + ), run: () => ( @@ -476,9 +557,13 @@ function SkipOnboardingHeaderLink() { function OnboardingProgress() { const s = stepper.useStepper(); - const steps = s.all; - const currentIndex = steps.findIndex((step) => step.id === s.current.id); - const total = steps.length; + const { isStepVisible, visibleStepIndex, visibleStepCount } = + useContext(StepVisibilityContext); + // Count only steps visible for the current path (agentOS adds steps that are + // hidden for the actor path), so "Step X of N" and the dots stay accurate. + const visibleSteps = s.all.filter((step) => isStepVisible(step.id)); + const currentIndex = Math.max(0, visibleStepIndex(s.current.id)); + const total = visibleStepCount; const groupLabel = s.current.group === "local" ? "Local setup" : "Deploy"; return ( @@ -494,7 +579,7 @@ function OnboardingProgress() { Step {currentIndex + 1} of {total} ยท {groupLabel}
- {steps.map((step, i) => ( + {visibleSteps.map((step, i) => (
+ +
+

agentOS needs an LLM key

+

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

+
+
+ ); +} + function OrDivider({ label }: { label: string }) { return (
@@ -653,9 +769,136 @@ 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="An open-source OS for agents. Runs in-process with ~6 ms cold starts." + isSelected={field.value === "agent-os"} + onSelect={() => + setValue("template", "agent-os", { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }) + } + /> +
+
+ )} + /> + ); +} + function InstallStep() { + const isAgentOs = useWatch({ name: "template" }) === "agent-os"; return (
+ {features.agentOs ? : null}
@@ -674,8 +917,9 @@ function InstallStep() {

Add the RivetKit package

- Install the library your app imports to define and call - Actors. + {isAgentOs + ? "Install the framework. You'll choose your agent and software packages in the next steps." + : "Install the library your app imports to define and call Actors."}

; + } return (
+ buildAgentOsSetup({ + agent: agent ?? DEFAULT_AGENT, + packages: packages ?? DEFAULT_PACKAGES, + sandbox: sandbox ?? { + enabled: false, + provider: DEFAULT_SANDBOX_PROVIDER, + }, + }), + [agent, packages, sandbox], + ); + + return ( +
+ + +
+
+

Install

+ +
+ + {[ + setup.serverCode} + className="m-0" + > + + , + setup.clientCode} + className="m-0" + > + + , + ]} + +
+
+ ); +} + function StepNumber({ n }: { n: number }) { return (
@@ -1159,6 +1476,7 @@ function BackendSetupRivet() { const { data: cloudToken } = useSuspenseQuery( dataProvider.createApiTokenQueryOptions({ name: "Onboarding" }), ); + const isAgentOs = useWatch({ name: "template" }) === "agent-os"; const ghSecretCmd = cloudToken ? `gh secret set RIVET_CLOUD_TOKEN --body "${cloudToken}"` @@ -1166,6 +1484,7 @@ function BackendSetupRivet() { return (
+ {isAgentOs ? : null}
@@ -1278,6 +1597,7 @@ function BackendSetupRivet() { function BackendSetup() { const provider = useWatch({ name: "provider" }); + const isAgentOs = useWatch({ name: "template" }) === "agent-os"; const mode = useWatch({ name: "mode" }) as | "serverless" | "serverfull" @@ -1290,6 +1610,7 @@ function BackendSetup() { return (
+ {isAgentOs ? : null}
@@ -1477,6 +1798,7 @@ function FrontendSetup() { }); const provider = useWatch({ name: "provider" }); + const isAgentOs = useWatch({ name: "template" }) === "agent-os"; const nsDataProvider = useCloudNamespaceDataProvider(); const { namespace: namespaceParam } = useParams({ strict: false }) as { namespace: string }; const { data: nsData } = useQuery( @@ -1545,15 +1867,31 @@ function FrontendSetup() { /> ) : ( )}
diff --git a/frontend/src/components/actors/dialogs/create-actor-sheet.tsx b/frontend/src/components/actors/dialogs/create-actor-sheet.tsx index 2c2432302b..84494ffee4 100644 --- a/frontend/src/components/actors/dialogs/create-actor-sheet.tsx +++ b/frontend/src/components/actors/dialogs/create-actor-sheet.tsx @@ -22,9 +22,14 @@ import * as ActorCreateForm from "../form/actor-create-form"; interface CreateActorSheetProps { open: boolean; onOpenChange: (open: boolean) => void; + variant?: "actor" | "agent-os"; } -export function CreateActorSheet({ open, onOpenChange }: CreateActorSheetProps) { +export function CreateActorSheet({ + open, + onOpenChange, + variant = "actor", +}: CreateActorSheetProps) { const { mutateAsync } = useMutation( useDataProvider().createActorMutationOptions(), ); @@ -35,6 +40,14 @@ export function CreateActorSheet({ open, onOpenChange }: CreateActorSheetProps) const { copy } = useActorsView(); const [advancedOpen, setAdvancedOpen] = useState(false); + const isAgentOs = variant === "agent-os"; + const title = isAgentOs + ? "Create agentOS instance" + : copy.createActorModal.title(name); + const description = isAgentOs + ? "agentOS boots an isolated VM for this key. Choose the agentOS build and a key to identify this instance." + : copy.createActorModal.description; + return ( - - {copy.createActorModal.title(name)} - - - {copy.createActorModal.description} - + {title} + {description} {/* diff --git a/frontend/src/components/onboarding/agent-os/agent-os-onboarding.stories.tsx b/frontend/src/components/onboarding/agent-os/agent-os-onboarding.stories.tsx new file mode 100644 index 0000000000..0a7b30310a --- /dev/null +++ b/frontend/src/components/onboarding/agent-os/agent-os-onboarding.stories.tsx @@ -0,0 +1,99 @@ +import type { Story } from "@ladle/react"; +import { useState } from "react"; +import "../../../../.ladle/ladle.css"; +import { AgentSelect } from "./agent-select-step"; +import { + type AgentOsSelections, + buildAgentOsSetup, +} from "./build-agent-os-setup"; +import { DEFAULT_AGENT, DEFAULT_PACKAGES } from "./catalog"; +import { SandboxMount, type SandboxValue } from "./sandbox-mount-step"; +import { SoftwareSelect } from "./software-select-step"; + +// Integration story: the three agentOS selection steps wired to the pure +// `buildAgentOsSetup` generator, so the generated install command / server.ts / +// client.ts / handoff prompt update live as selections change. This is the unit +// that matters (selection -> generated code); the individual card lists are +// trivial on their own. +function Harness({ initial }: { initial: AgentOsSelections }) { + const [agent, setAgent] = useState(initial.agent); + const [packages, setPackages] = useState(initial.packages); + const [sandbox, setSandbox] = useState(initial.sandbox); + + const setup = buildAgentOsSetup({ agent, packages, sandbox }); + + return ( +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ {setup.installCommand} +
+
+ {setup.serverCode} +
+
+ {setup.clientCode} +
+
+ {setup.prompt} +
+
+
+
+ ); +} + +function Section({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( +
+

{title}

+ {children} +
+ ); +} + +function Code({ children }: { children: string }) { + return ( +
+			{children}
+		
+ ); +} + +export const Playground: Story = () => ( + +); + +export const WithSandboxAndExtras: Story = () => ( + +); diff --git a/frontend/src/components/onboarding/agent-os/agent-select-step.tsx b/frontend/src/components/onboarding/agent-os/agent-select-step.tsx new file mode 100644 index 0000000000..8fccf59bc6 --- /dev/null +++ b/frontend/src/components/onboarding/agent-os/agent-select-step.tsx @@ -0,0 +1,42 @@ +import { useController } from "react-hook-form"; +import { AGENTS, DEFAULT_AGENT } from "./catalog"; +import { SelectCard } from "./select-card"; + +// Presentational single-select for the coding agent. Pi is available; the rest +// render disabled with a "Coming soon" badge. +export function AgentSelect({ + value, + onChange, +}: { + value: string; + onChange: (slug: string) => void; +}) { + return ( +
+ {AGENTS.map((agent) => ( + onChange(agent.slug)} + /> + ))} +
+ ); +} + +// Wizard step: binds AgentSelect to the react-hook-form `agent` field. +export function AgentSelectStep() { + const { field } = useController({ name: "agent" }); + return ( + + ); +} diff --git a/frontend/src/components/onboarding/agent-os/build-agent-os-setup.ts b/frontend/src/components/onboarding/agent-os/build-agent-os-setup.ts new file mode 100644 index 0000000000..42f7b63db7 --- /dev/null +++ b/frontend/src/components/onboarding/agent-os/build-agent-os-setup.ts @@ -0,0 +1,219 @@ +// Pure generator: maps the onboarding agentOS selections to the install command, +// the server.ts/client.ts the user hands to their coding agent, and a handoff +// prompt that enumerates the selections so the agent scaffolds matching code. +// +// The generated code mirrors the verified in-repo examples: +// examples/agent-os/src/agent-session/server.ts (minimal) +// examples/agent-os/src/sandbox/server.ts (sandbox on) +// Software/agent packages are default imports passed to `software: [...]`. + +import { + AGENTS, + DEFAULT_AGENT, + DEFAULT_SANDBOX_PROVIDER, + SOFTWARE, + type SoftwareEntry, +} from "./catalog"; + +export interface AgentOsSelections { + agent: string; + packages: string[]; + sandbox: { enabled: boolean; provider?: string }; +} + +export interface AgentOsSetup { + installCommand: string; + serverCode: string; + clientCode: string; + prompt: string; +} + +// kebab-case slug -> a valid default-import identifier (e.g. build-essential -> buildEssential). +function importName(slug: string): string { + return slug.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()); +} + +function resolveAgent(slug: string) { + const found = AGENTS.find((a) => a.slug === slug && a.status === "available"); + // Fall back to the default available agent (Pi) if an unavailable one slips through. + return found?.package + ? found + : (AGENTS.find((a) => a.slug === DEFAULT_AGENT) ?? AGENTS[0]); +} + +export function buildAgentOsSetup(selections: AgentOsSelections): AgentOsSetup { + const agent = resolveAgent(selections.agent); + const agentPackage = agent.package ?? "@rivet-dev/agent-os-pi"; + const agentSymbol = importName(agent.slug); + + const softwareEntries: SoftwareEntry[] = selections.packages + .map((slug) => SOFTWARE.find((s) => s.slug === slug)) + .filter((s): s is SoftwareEntry => Boolean(s)); + + const sandboxEnabled = selections.sandbox.enabled; + const provider = selections.sandbox.provider ?? DEFAULT_SANDBOX_PROVIDER; + + const installPackages = [ + "rivetkit", + ...softwareEntries.map((s) => s.package), + agentPackage, + ...(sandboxEnabled + ? ["@rivet-dev/agent-os-sandbox", "sandbox-agent"] + : []), + ]; + const installCommand = `npm install ${installPackages.join(" ")}`; + + const softwareSymbols = [ + ...softwareEntries.map((s) => importName(s.slug)), + agentSymbol, + ]; + + const importLines = [ + `import { agentOs } from "rivetkit/agent-os";`, + `import { setup } from "rivetkit";`, + ...softwareEntries.map( + (s) => `import ${importName(s.slug)} from "${s.package}";`, + ), + `import ${agentSymbol} from "${agentPackage}";`, + ...(sandboxEnabled + ? [ + `import { SandboxAgent } from "sandbox-agent";`, + `import { ${provider} } from "sandbox-agent/${provider}";`, + `import { createSandboxFs, createSandboxToolkit } from "@rivet-dev/agent-os-sandbox";`, + ] + : []), + ]; + + const softwareArray = softwareSymbols.join(", "); + + const sandboxSetup = sandboxEnabled + ? `\n// Start a ${provider}-backed sandbox, mounted at /sandbox.\nconst sandbox = await SandboxAgent.start({ sandbox: ${provider}() });\n` + : ""; + + const optionsBlock = sandboxEnabled + ? ` options: { + software: [${softwareArray}], + mounts: [{ path: "/sandbox", driver: createSandboxFs({ client: sandbox }) }], + toolKits: [createSandboxToolkit({ client: sandbox })], + },` + : ` options: { software: [${softwareArray}] },`; + + const serverCode = `${importLines.join("\n")} +${sandboxSetup} +const vm = agentOs({ +${optionsBlock} +}); + +export const registry = setup({ use: { vm } }); +registry.start(); +`; + + const clientCode = `import { createClient } from "rivetkit/client"; +import type { registry } from "./server"; + +const client = createClient(); + +// getOrCreate boots the agentOS instance on first call. +const agent = client.vm.getOrCreate(["my-agent"]); + +agent.on("sessionEvent", (data) => console.log(data.event)); + +const session = await agent.createSession("${agent.slug}", { + 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)); +`; + + const prompt = buildPrompt({ + agentTitle: agent.title, + softwareEntries, + sandboxEnabled, + provider, + installCommand, + serverCode, + clientCode, + }); + + return { installCommand, serverCode, clientCode, prompt }; +} + +function buildPrompt(opts: { + agentTitle: string; + softwareEntries: SoftwareEntry[]; + sandboxEnabled: boolean; + provider: string; + installCommand: string; + serverCode: string; + clientCode: string; +}): string { + const { + agentTitle, + softwareEntries, + sandboxEnabled, + provider, + installCommand, + serverCode, + clientCode, + } = opts; + + const softwareList = + softwareEntries.length > 0 + ? softwareEntries.map((s) => `- ${s.title} (${s.package})`).join("\n") + : "- (none beyond the agent)"; + + const sandboxSection = sandboxEnabled + ? `\n## Sandbox mounting + +This setup mounts a ${provider} sandbox at \`/sandbox\` for heavy workloads. It requires the \`sandbox-agent\` and \`@rivet-dev/agent-os-sandbox\` packages and a running ${provider} provider. See https://rivet.dev/docs/agent-os/sandbox. +` + : ""; + + return `# agentOS Setup + +I want to add agentOS to this project using RivetKit. agentOS is a portable, open-source operating system for agents that runs in your process. Software is baked into the build at build time and is immutable after deploy, so these choices are made up front. An agentOS actor is a normal Rivet Actor and deploys like any other. + +Read https://rivet.dev/docs/agent-os/quickstart before making changes. agentOS is in preview. + +## Selections + +Agent: ${agentTitle} +Software baked into the build: +${softwareList} +Sandbox mounting: ${sandboxEnabled ? `enabled (${provider})` : "disabled"} + +## Steps + +### Step 1: Install + +\`\`\`bash +${installCommand} +\`\`\` + +### Step 2: Create the server (server.ts) + +\`\`\`ts +${serverCode}\`\`\` + +### 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 (client.ts) + +\`\`\`ts +${clientCode}\`\`\` +${sandboxSection} +### 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).`; +} diff --git a/frontend/src/components/onboarding/agent-os/catalog.ts b/frontend/src/components/onboarding/agent-os/catalog.ts new file mode 100644 index 0000000000..4069eff18b --- /dev/null +++ b/frontend/src/components/onboarding/agent-os/catalog.ts @@ -0,0 +1,176 @@ +// Curated agentOS registry catalog for the onboarding selection steps. +// +// The canonical registry lives at website/src/data/registry.ts, but it is +// website-only (the dashboard has no access to it). This is a curated subset of +// the entries relevant to onboarding. A future unification into +// frontend/packages/shared-data (where deploy.ts already lives) would remove the +// drift; out of scope for now. + +export type CatalogStatus = "available" | "coming-soon"; + +export interface AgentEntry { + slug: string; + title: string; + description: string; + status: CatalogStatus; + /** Present only when status is "available". */ + package?: string; +} + +export interface SoftwareEntry { + slug: string; + title: string; + package: string; + description: string; +} + +export interface SandboxProviderEntry { + slug: string; + title: string; + description: string; + /** + * Only `docker` is verified against an in-repo example + * (examples/agent-os/src/sandbox/server.ts). Other providers follow the + * `sandbox-agent/` subpath + same-named factory pattern and should be + * confirmed before relying on the generated import. + */ + verified?: boolean; +} + +// Coding agents. Only Pi is available today; the rest render disabled. +export const AGENTS: AgentEntry[] = [ + { + slug: "pi", + title: "Pi", + description: "Lightweight, fast coding agent.", + status: "available", + package: "@rivet-dev/agent-os-pi", + }, + { + slug: "claude-code", + title: "Claude Code", + description: "Coming soon.", + status: "coming-soon", + }, + { + slug: "codex", + title: "Codex", + description: "Coming soon.", + status: "coming-soon", + }, + { + slug: "amp", + title: "Amp", + description: "Coming soon.", + status: "coming-soon", + }, + { + slug: "opencode", + title: "OpenCode", + description: "Coming soon.", + status: "coming-soon", + }, +]; + +// Software packages baked into the build. `common` is on by default. +export const SOFTWARE: SoftwareEntry[] = [ + { + slug: "common", + title: "Common", + package: "@rivet-dev/agent-os-common", + description: + "coreutils, sed, grep, gawk, findutils, diffutils, tar, gzip.", + }, + { + slug: "build-essential", + title: "Build Essential", + package: "@rivet-dev/agent-os-build-essential", + description: "common + make + git + curl.", + }, + { + slug: "git", + title: "git", + package: "@rivet-dev/agent-os-git", + description: "Version control.", + }, + { + slug: "jq", + title: "jq", + package: "@rivet-dev/agent-os-jq", + description: "Lightweight JSON processor.", + }, + { + slug: "ripgrep", + title: "ripgrep", + package: "@rivet-dev/agent-os-ripgrep", + description: "Fast recursive search (rg).", + }, + { + slug: "fd", + title: "fd", + package: "@rivet-dev/agent-os-fd", + description: "Fast file finder.", + }, + { + slug: "tree", + title: "tree", + package: "@rivet-dev/agent-os-tree", + description: "Display directory structure as a tree.", + }, + { + slug: "coreutils", + title: "Coreutils", + package: "@rivet-dev/agent-os-coreutils", + description: "Essential POSIX commands (when not using Common).", + }, +]; + +export const DEFAULT_AGENT = "pi"; +export const DEFAULT_PACKAGES = ["common"]; + +// Sandbox mounting providers (sandbox-agent). Mounted at /sandbox. +export const SANDBOX_PROVIDERS: SandboxProviderEntry[] = [ + { + slug: "docker", + title: "Docker", + description: "Run sandboxes in local Docker containers.", + verified: true, + }, + { + slug: "e2b", + title: "E2B", + description: "Secure, ephemeral cloud sandboxes.", + }, + { + slug: "local", + title: "Local", + description: "Run sandboxes directly on the local machine.", + }, + { + slug: "daytona", + title: "Daytona", + description: "Managed development environments.", + }, + { + slug: "modal", + title: "Modal", + description: "Serverless cloud sandboxes.", + }, + { + slug: "vercel", + title: "Vercel", + description: "Vercel's edge and serverless platform.", + }, + { + slug: "computesdk", + title: "ComputeSDK", + description: "The ComputeSDK compute provider.", + }, + { + slug: "sprites", + title: "Sprites", + description: "Sprites' cloud sandbox infrastructure.", + }, +]; + +export const DEFAULT_SANDBOX_PROVIDER = "docker"; diff --git a/frontend/src/components/onboarding/agent-os/sandbox-mount-step.tsx b/frontend/src/components/onboarding/agent-os/sandbox-mount-step.tsx new file mode 100644 index 0000000000..63acbf2045 --- /dev/null +++ b/frontend/src/components/onboarding/agent-os/sandbox-mount-step.tsx @@ -0,0 +1,64 @@ +import { useController } from "react-hook-form"; +import { DEFAULT_SANDBOX_PROVIDER, SANDBOX_PROVIDERS } from "./catalog"; +import { SelectCard } from "./select-card"; + +export interface SandboxValue { + enabled: boolean; + provider?: string; +} + +// Presentational sandbox-mounting selector. Off by default; when on, pick a +// provider mounted at /sandbox. +export function SandboxMount({ + value, + onChange, +}: { + value: SandboxValue; + onChange: (value: SandboxValue) => void; +}) { + const provider = value.provider ?? DEFAULT_SANDBOX_PROVIDER; + + return ( +
+ + onChange({ ...value, enabled: !value.enabled }) + } + /> + {value.enabled ? ( +
+

+ Provider (mounted at /sandbox) +

+
+ {SANDBOX_PROVIDERS.map((p) => ( + + onChange({ ...value, provider: p.slug }) + } + /> + ))} +
+
+ ) : null} +
+ ); +} + +// Wizard step: binds SandboxMount to the react-hook-form `sandbox` field. +export function SandboxMountStep() { + const { field } = useController({ name: "sandbox" }); + const value = (field.value as SandboxValue) ?? { + enabled: false, + provider: DEFAULT_SANDBOX_PROVIDER, + }; + return ; +} diff --git a/frontend/src/components/onboarding/agent-os/select-card.tsx b/frontend/src/components/onboarding/agent-os/select-card.tsx new file mode 100644 index 0000000000..ea43b24d2f --- /dev/null +++ b/frontend/src/components/onboarding/agent-os/select-card.tsx @@ -0,0 +1,78 @@ +import { faCheck, Icon } from "@rivet-gg/icons"; +import type { ReactNode } from "react"; +import { cn } from "@/components"; +import { Badge } from "@/components/ui/badge"; + +// Shared selection card for the agentOS onboarding steps. Mirrors the +// BuildTargetSelector card styling so the agentOS steps feel cohesive with the +// rest of the wizard. Single-select cards highlight; multi-select cards show a +// checkbox indicator. +export function SelectCard({ + title, + description, + badge, + icon, + selected, + disabled, + multi, + onSelect, +}: { + title: string; + description: string; + badge?: string; + icon?: ReactNode; + selected: boolean; + disabled?: boolean; + multi?: boolean; + onSelect: () => void; +}) { + return ( + + ); +} 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 ( + + ); +} diff --git a/frontend/src/lib/features.ts b/frontend/src/lib/features.ts index bd46bfa559..39e9b2e03e 100644 --- a/frontend/src/lib/features.ts +++ b/frontend/src/lib/features.ts @@ -30,6 +30,8 @@ export const features = { // deployments, logs, and the Rivet provider option. Cloud-platform-only // because every surface consumes cloud-namespace data providers. compute: isEnabled("compute") && platform, + // `agentOs` gates the agentOS (coding-agent VM) onboarding template. Preview. + agentOs: isEnabled("agent-os"), support: isEnabled("support"), branding: isEnabled("branding"), datacenter: isEnabled("datacenter"), diff --git a/frontend/src/routes/_context/ns.$namespace.tsx b/frontend/src/routes/_context/ns.$namespace.tsx index d29feee2f3..223d892957 100644 --- a/frontend/src/routes/_context/ns.$namespace.tsx +++ b/frontend/src/routes/_context/ns.$namespace.tsx @@ -194,6 +194,21 @@ function Modals() { } }} /> + { + if (!value) { + return navigate({ + to: ".", + search: (old) => ({ + ...old, + modal: undefined, + }), + }); + } + }} + /> + { + if (!value) { + return navigate({ + to: ".", + search: (old) => ({ + ...old, + modal: undefined, + }), + }); + } + }} + />