);
}
@@ -262,28 +311,29 @@ function FeatureCard({
flex: 1,
display: "flex",
flexDirection: "column",
- gap: 12,
- borderRadius: 28,
- padding: 24,
- background: "rgba(15, 23, 42, 0.82)",
- border: "1px solid rgba(148, 163, 184, 0.18)",
+ gap: 10,
+ borderRadius: 16,
+ padding: 20,
+ background: "rgba(255, 255, 255, 0.03)",
+ border: "1px solid rgba(255, 255, 255, 0.08)",
}}
>
{detail}
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 90411b945..261841732 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -52,11 +52,15 @@ export const metadata: Metadata = {
default: "Open Agents",
template: "%s | Open Agents",
},
- description: "Open Agents web app for managing AI coding sessions.",
+ description:
+ "Spawn coding agents that run infinitely in the cloud. Powered by AI SDK, Gateway, Sandbox, and Workflow DevKit.",
icons: {
icon: faviconPath,
shortcut: faviconPath,
},
+ twitter: {
+ card: "summary_large_image",
+ },
};
export default function RootLayout({
diff --git a/apps/web/app/opengraph-image.tsx b/apps/web/app/opengraph-image.tsx
new file mode 100644
index 000000000..2feee78cd
--- /dev/null
+++ b/apps/web/app/opengraph-image.tsx
@@ -0,0 +1,200 @@
+import { ImageResponse } from "next/og";
+
+export const alt = "Open Agents — Spawn coding agents that run in the cloud";
+export const size = { width: 1200, height: 630 };
+export const contentType = "image/png";
+export const runtime = "edge";
+
+export default function OgImage() {
+ return new ImageResponse(
+
+ {/* Subtle radial glow — top-left warm, bottom-right cool */}
+
+
+ {/* Noise-ish grain approximation with faint horizontal lines */}
+
+
+ {/* Border frame */}
+
+
+ {/* Content */}
+
+ {/* Top section */}
+
+ {/* Logo / icon + wordmark row */}
+
+
+
+ Open Agents
+
+
+
+ {/* Hero heading */}
+
+ Open Agents.
+
+
+ {/* Subtitle */}
+
+ Spawn coding agents that run infinitely in the cloud.
+
+
+
+ {/* Bottom row — tech pills */}
+
+
+
+
+
+
+ {/* Spacer + domain */}
+
+
+ open-agents.dev
+
+
+
+
+
,
+ { ...size },
+ );
+}
+
+function TechPill({ label }: { label: string }) {
+ return (
+
+ {label}
+
+ );
+}
diff --git a/apps/web/app/shared/[shareId]/opengraph-image.tsx b/apps/web/app/shared/[shareId]/opengraph-image.tsx
new file mode 100644
index 000000000..398f48f46
--- /dev/null
+++ b/apps/web/app/shared/[shareId]/opengraph-image.tsx
@@ -0,0 +1,238 @@
+import { ImageResponse } from "next/og";
+import { eq } from "drizzle-orm";
+import { db } from "@/lib/db/client";
+import { users } from "@/lib/db/schema";
+import { getChatById } from "@/lib/db/sessions";
+import {
+ getSessionByIdCached,
+ getShareByIdCached,
+} from "@/lib/db/sessions-cache";
+
+export const alt = "Shared Open Agents session";
+export const size = { width: 1200, height: 630 };
+export const contentType = "image/png";
+
+export default async function Image({
+ params,
+}: {
+ params: Promise<{ shareId: string }>;
+}) {
+ const { shareId } = await params;
+
+ const share = await getShareByIdCached(shareId);
+ if (!share) {
+ return fallbackImage();
+ }
+
+ const chat = await getChatById(share.chatId);
+ if (!chat) {
+ return fallbackImage();
+ }
+
+ const session = await getSessionByIdCached(chat.sessionId);
+ if (!session) {
+ return fallbackImage();
+ }
+
+ const [owner] = await db
+ .select({
+ username: users.username,
+ name: users.name,
+ avatarUrl: users.avatarUrl,
+ })
+ .from(users)
+ .where(eq(users.id, session.userId))
+ .limit(1);
+
+ if (!owner) {
+ return fallbackImage();
+ }
+
+ const displayName = owner.name?.trim() || owner.username;
+ const repoLabel =
+ session.repoOwner && session.repoName
+ ? `${session.repoOwner}/${session.repoName}`
+ : null;
+
+ return new ImageResponse(
+
+
+
+
+
+
+
+
+
+
+ Open Agents
+
+
+
+
+ {chat.title || "Shared Chat"}
+
+
+ {repoLabel ? (
+
+ {repoLabel}
+
+ ) : null}
+
+ {session.branch ? (
+
+ Branch: {session.branch}
+
+ ) : null}
+
+
+
+
+ Shared by {displayName}
+
+
+ open-agents.dev
+
+
+
+
,
+ { ...size },
+ );
+}
+
+function fallbackImage() {
+ return new ImageResponse(
+
+ Shared Open Agents session
+
,
+ { ...size },
+ );
+}
diff --git a/apps/web/app/twitter-image.tsx b/apps/web/app/twitter-image.tsx
new file mode 100644
index 000000000..ecffdd060
--- /dev/null
+++ b/apps/web/app/twitter-image.tsx
@@ -0,0 +1,3 @@
+export { default, alt, size, contentType } from "./opengraph-image";
+
+export const runtime = "edge";
diff --git a/apps/web/public/.well-known/workflow/v1/manifest.json b/apps/web/public/.well-known/workflow/v1/manifest.json
index eea5512e3..0c0715132 100644
--- a/apps/web/public/.well-known/workflow/v1/manifest.json
+++ b/apps/web/public/.well-known/workflow/v1/manifest.json
@@ -6,17 +6,6 @@
"stepId": "step//workflow@4.2.0-beta.74//fetch"
}
},
- "app/workflows/sandbox-lifecycle.ts": {
- "clearLifecycleRunIdIfOwned": {
- "stepId": "step//./app/workflows/sandbox-lifecycle//clearLifecycleRunIdIfOwned"
- },
- "computeLifecycleWakeDecision": {
- "stepId": "step//./app/workflows/sandbox-lifecycle//computeLifecycleWakeDecision"
- },
- "runLifecycleEvaluation": {
- "stepId": "step//./app/workflows/sandbox-lifecycle//runLifecycleEvaluation"
- }
- },
"node_modules/workflow/dist/internal/builtins.js": {
"__builtin_response_array_buffer": {
"stepId": "__builtin_response_array_buffer"
@@ -28,6 +17,17 @@
"stepId": "__builtin_response_text"
}
},
+ "app/workflows/sandbox-lifecycle.ts": {
+ "clearLifecycleRunIdIfOwned": {
+ "stepId": "step//./app/workflows/sandbox-lifecycle//clearLifecycleRunIdIfOwned"
+ },
+ "computeLifecycleWakeDecision": {
+ "stepId": "step//./app/workflows/sandbox-lifecycle//computeLifecycleWakeDecision"
+ },
+ "runLifecycleEvaluation": {
+ "stepId": "step//./app/workflows/sandbox-lifecycle//runLifecycleEvaluation"
+ }
+ },
"app/workflows/chat-post-finish.ts": {
"clearActiveStream": {
"stepId": "step//./app/workflows/chat-post-finish//clearActiveStream"