diff --git a/frontend/public/images/brand/actors-mark.svg b/frontend/public/images/brand/actors-mark.svg
new file mode 100644
index 0000000000..3a94d564fa
--- /dev/null
+++ b/frontend/public/images/brand/actors-mark.svg
@@ -0,0 +1,21 @@
+
diff --git a/frontend/public/images/brand/agentos-mark.svg b/frontend/public/images/brand/agentos-mark.svg
new file mode 100644
index 0000000000..d5e05f022a
--- /dev/null
+++ b/frontend/public/images/brand/agentos-mark.svg
@@ -0,0 +1,13 @@
+
diff --git a/frontend/public/images/brand/dynamic-apps-mark.svg b/frontend/public/images/brand/dynamic-apps-mark.svg
new file mode 100644
index 0000000000..c89025cc3c
--- /dev/null
+++ b/frontend/public/images/brand/dynamic-apps-mark.svg
@@ -0,0 +1,9 @@
+
diff --git a/frontend/public/images/brand/workflows-mark.svg b/frontend/public/images/brand/workflows-mark.svg
new file mode 100644
index 0000000000..8f4f7b2229
--- /dev/null
+++ b/frontend/public/images/brand/workflows-mark.svg
@@ -0,0 +1,9 @@
+
diff --git a/frontend/src/app/actors-grid.tsx b/frontend/src/app/actors-grid.tsx
index 4f59f65964..a0ab9a8d3d 100644
--- a/frontend/src/app/actors-grid.tsx
+++ b/frontend/src/app/actors-grid.tsx
@@ -1,4 +1,4 @@
-import { faChevronDown, faGear, faLogs, faPlus, Icon } from "@rivet-gg/icons";
+import { faGear, faLogs, Icon } from "@rivet-gg/icons";
import {
queryOptions,
useInfiniteQuery,
@@ -22,18 +22,12 @@ import {
useCloudNamespaceDataProvider,
useDataProvider,
} from "@/components/actors";
-import { Badge } from "@/components/ui/badge";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
import { NoProvidersAlert } from "@/components/actors/no-providers-alert";
import { ActorIcon } from "@/components/lazy-icon";
import { VisibilitySensor } from "@/components/visibility-sensor";
import { features } from "@/lib/features";
import { getRivetRunUrl } from "../lib/env";
+import { AddComponentButton, AddComponentCard } from "./add-component-card";
import { RouteLayout } from "./route-layout";
function _GridCard({
@@ -65,61 +59,6 @@ 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 (
- }
- onClick={() => openModal("create-actor")}
- >
- Create Actor
-
- );
- }
-
- return (
-
-
- }
- endIcon={}
- >
- Create
-
-
-
- openModal("create-actor")}>
- Actor
-
- openModal("create-agent-os")}>
- agentOS
-
- Beta
-
-
-
-
- );
-}
-
export function ActorGridCardSkeleton() {
return (
@@ -269,9 +208,6 @@ export function ActorsGrid({ namespaceLabel }: { namespaceLabel?: string }) {
Actors
- {builds.length > 0 ? (
-
- ) : null}
{isLoading ? (
@@ -293,7 +229,7 @@ export function ActorsGrid({ namespaceLabel }: { namespaceLabel?: string }) {
Deploy code that registers an actor to
see it here.
-
+
)
) : (
@@ -305,6 +241,7 @@ export function ActorsGrid({ namespaceLabel }: { namespaceLabel?: string }) {
build={build}
/>
))}
+
{isFetchingNextPage
? Array.from({ length: 4 }).map(
(_, i) => (
diff --git a/frontend/src/app/add-component-card.tsx b/frontend/src/app/add-component-card.tsx
new file mode 100644
index 0000000000..0bd816d47b
--- /dev/null
+++ b/frontend/src/app/add-component-card.tsx
@@ -0,0 +1,64 @@
+import { faPlus, Icon } from "@rivet-gg/icons";
+import { type ReactNode, useState } from "react";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/components/lib/utils";
+import { useDialog } from "./use-dialog";
+
+function AddComponentDialogTrigger({
+ children,
+}: {
+ children: (open: () => void) => ReactNode;
+}) {
+ const [isOpen, setOpen] = useState(false);
+ const Dialog = useDialog.AddComponent.Dialog;
+ return (
+ <>
+ {children(() => setOpen(true))}
+
+ >
+ );
+}
+
+export function AddComponentButton() {
+ return (
+
+ {(open) => (
+ }
+ onClick={open}
+ >
+ Add a component
+
+ )}
+
+ );
+}
+
+// Matches ActorBuildCard's shape so it reads as the last tile in the grid
+// rather than a control that happens to sit next to it.
+export function AddComponentCard() {
+ return (
+
+ {(open) => (
+
+ )}
+
+ );
+}
diff --git a/frontend/src/app/compute-deploy.tsx b/frontend/src/app/compute-deploy.tsx
index 7f48df2296..3e14538f75 100644
--- a/frontend/src/app/compute-deploy.tsx
+++ b/frontend/src/app/compute-deploy.tsx
@@ -1,17 +1,26 @@
import { faCopy, Icon } from "@rivet-gg/icons";
import { deployOptions, type Provider } from "@rivetkit/shared-data";
import { useSuspenseQuery } from "@tanstack/react-query";
+import { useParams } from "@tanstack/react-router";
import { toast } from "sonner";
-import { Badge, Button, CodeFrame, CodePreview } from "@/components";
+import { Badge, Button, CodeFrame, CodePreview, getConfig } from "@/components";
import {
useCloudNamespaceDataProvider,
useEngineCompatDataProvider,
} from "@/components/actors";
+import {
+ hostedMcpCommand,
+ localMcpCommand,
+} from "@/components/mcp/client-tabs";
+import { type HostedTarget, hostedUrl } from "@/components/mcp/scope";
import {
getAgentInstructionsPrompt,
getComputeAddendum,
+ type McpSetup,
+ type OnboardingTarget,
} from "@/content/agent-prompts";
-import { cloudEnv, getRivetRunUrl } from "@/lib/env";
+import { cloudEnv, getMcpUrl, getRivetRunUrl } from "@/lib/env";
+import { features } from "@/lib/features";
import { usePublishableToken } from "@/queries/accessors";
import { useRivetDsn } from "./env-variables";
@@ -40,11 +49,13 @@ export function useAgentInstructionsCode({
runnerName = "default",
endpoint,
mode,
+ target = "actor",
}: {
provider?: Provider;
runnerName?: string;
endpoint?: string;
mode?: "serverless" | "serverful";
+ target?: OnboardingTarget;
} = {}) {
const providerDetails = provider
? deployOptions.find((p) => p.name === provider)
@@ -62,6 +73,7 @@ export function useAgentInstructionsCode({
(mode ?? defaultRuntimeModeForProvider(provider)) === "serverless";
const publishableToken = useRivetDsn({ kind: "publishable", endpoint });
const secretToken = useRivetDsn({ kind: "secret", endpoint });
+ const mcp = useMcpSetup();
const namespace = useEngineCompatDataProvider().engineNamespace;
return getAgentInstructionsPrompt({
@@ -75,20 +87,63 @@ export function useAgentInstructionsCode({
// The `--namespace` deploy flag only applies to Rivet Compute's
// `@rivetkit/cli deploy` flow.
cliDeploy: provider === "rivet",
+ target,
+ mcp,
});
}
+// The MCP setup the copy-prompt should instruct the agent to perform. The hosted
+// connection needs the user to approve an OAuth window, so the agent has to hand
+// that step back; the local stdio server it can wire up itself.
+function useMcpSetup(): McpSetup | undefined {
+ const params = useParams({ strict: false }) as Partial;
+ const dataProvider = useEngineCompatDataProvider();
+
+ if (!features.mcp) return undefined;
+
+ if (features.platform) {
+ if (!params.organization || !params.project || !params.namespace) {
+ return undefined;
+ }
+ const url = hostedUrl(
+ getMcpUrl(),
+ {
+ organization: params.organization,
+ project: params.project,
+ namespace: params.namespace,
+ },
+ "namespace",
+ );
+ return {
+ command: hostedMcpCommand(url),
+ requiresUserApproval: true,
+ };
+ }
+
+ return {
+ command: localMcpCommand(
+ getConfig().apiUrl,
+ dataProvider.engineNamespace,
+ ),
+ requiresUserApproval: false,
+ };
+}
+
// 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" });
+export function useComputeInstructionsCode(target: OnboardingTarget = "actor") {
+ const agentInstructions = useAgentInstructionsCode({
+ provider: "rivet",
+ target,
+ });
const dataProvider = useCloudNamespaceDataProvider();
const { data: cloudToken } = useSuspenseQuery(
dataProvider.createApiTokenQueryOptions({ name: "Onboarding" }),
);
const publishableRawToken = usePublishableToken();
const namespace = dataProvider.engineNamespace;
+ const mcp = useMcpSetup();
const computeAddendum = getComputeAddendum({
cloudToken,
@@ -97,6 +152,8 @@ export function useComputeInstructionsCode() {
apiUrl: cloudEnv().VITE_APP_API_URL,
cloudApiUrl: cloudEnv().VITE_APP_CLOUD_API_URL,
rivetRunUrl: getRivetRunUrl(namespace),
+ target,
+ mcp,
});
return {
@@ -143,7 +200,7 @@ export function AgentPromptBanner({
: "Copied to clipboard",
);
}}
- className="relative w-full flex items-center justify-between gap-4 rounded-lg px-4 py-4 border border-primary group cursor-pointer text-left"
+ className="relative w-full flex flex-col items-stretch justify-between gap-4 rounded-lg px-4 py-4 border border-primary group cursor-pointer text-left sm:flex-row sm:items-center"
>
Recommended
@@ -158,7 +215,11 @@ export function AgentPromptBanner({
) : null}
-