diff --git a/frontend/packages/inspector/index.html b/frontend/packages/inspector/index.html index b7b284d..259bce8 100644 --- a/frontend/packages/inspector/index.html +++ b/frontend/packages/inspector/index.html @@ -5,31 +5,36 @@ Sandbox Agent + + + + diff --git a/frontend/packages/inspector/src/components/chat/ChatMessages.tsx b/frontend/packages/inspector/src/components/chat/ChatMessages.tsx index 3c7c2cc..75f1ee1 100644 --- a/frontend/packages/inspector/src/components/chat/ChatMessages.tsx +++ b/frontend/packages/inspector/src/components/chat/ChatMessages.tsx @@ -1,102 +1,150 @@ +import { useState } from "react"; import { getAvatarLabel, getMessageClass } from "./messageUtils"; +import renderContentPart from "./renderContentPart"; import type { TimelineEntry } from "./types"; -import { formatJson } from "../../utils/format"; +import { AlertTriangle, Settings, ChevronRight, ChevronDown } from "lucide-react"; + +const CollapsibleMessage = ({ + id, + icon, + label, + children, + className = "" +}: { + id: string; + icon: React.ReactNode; + label: string; + children: React.ReactNode; + className?: string; +}) => { + const [expanded, setExpanded] = useState(false); + + return ( +
+ + {expanded &&
{children}
} +
+ ); +}; const ChatMessages = ({ entries, sessionError, + eventError, messagesEndRef }: { entries: TimelineEntry[]; sessionError: string | null; + eventError: string | null; messagesEndRef: React.RefObject; }) => { return (
{entries.map((entry) => { - const messageClass = getMessageClass(entry); - if (entry.kind === "meta") { - return ( -
-
{getAvatarLabel(messageClass)}
-
-
- {entry.meta?.title ?? "Status"} -
- {entry.meta?.detail &&
{entry.meta.detail}
} + const isError = entry.meta?.severity === "error"; + const title = entry.meta?.title ?? "Status"; + const isStatusDivider = ["Session Started", "Turn Started", "Turn Ended"].includes(title); + + if (isStatusDivider) { + return ( +
+
+ + + {title} + +
-
+ ); + } + + // Other status messages - collapsible + return ( + : } + label={title} + className={isError ? "error" : "system"} + > + {entry.meta?.detail &&
{entry.meta.detail}
} +
); } - if (entry.kind === "reasoning") { + const item = entry.item; + if (!item) return null; + const hasParts = (item.content ?? []).length > 0; + const isInProgress = item.status === "in_progress"; + const isFailed = item.status === "failed"; + const messageClass = getMessageClass(item); + const statusLabel = item.status !== "completed" ? item.status.replace("_", " ") : ""; + const kindLabel = item.kind.replace("_", " "); + const isTool = messageClass === "tool"; + + // Tool results - collapsible + if (isTool) { return ( -
-
AI
-
-
- reasoning - {entry.reasoning?.visibility ?? "public"} -
-
{entry.reasoning?.text ?? ""}
-
-
+ T} + label={`${kindLabel}${statusLabel ? ` (${statusLabel})` : ""}`} + className="tool" + > + {hasParts ? ( + (item.content ?? []).map(renderContentPart) + ) : entry.deltaText ? ( + {entry.deltaText} + ) : ( + No content. + )} + ); } - if (entry.kind === "tool") { - const isComplete = entry.toolStatus === "completed" || entry.toolStatus === "failed"; - const isFailed = entry.toolStatus === "failed"; - return ( -
-
{getAvatarLabel(isFailed ? "error" : "tool")}
-
+ return ( +
+ {!isFailed &&
{getAvatarLabel(messageClass)}
} +
+ {(item.kind !== "message" || item.status !== "completed") && (
- tool call - {entry.toolName} - {entry.toolStatus && entry.toolStatus !== "completed" && ( - - {entry.toolStatus.replace("_", " ")} + {isFailed && } + {kindLabel} + {statusLabel && ( + + {statusLabel} )}
- {entry.toolInput &&
{entry.toolInput}
} - {isComplete && entry.toolOutput && ( -
-
result
-
{entry.toolOutput}
-
- )} - {!isComplete && !entry.toolInput && ( - - - - - - )} -
-
- ); - } - - // Message (user or assistant) - return ( -
-
{getAvatarLabel(messageClass)}
-
- {entry.text ? ( -
{entry.text}
- ) : ( + )} + {hasParts ? ( + (item.content ?? []).map(renderContentPart) + ) : entry.deltaText ? ( + + {entry.deltaText} + {isInProgress && } + + ) : isInProgress ? ( + ) : ( + No content yet. )}
); })} {sessionError &&
{sessionError}
} + {eventError &&
{eventError}
}
); diff --git a/frontend/packages/inspector/src/components/chat/messageUtils.ts b/frontend/packages/inspector/src/components/chat/messageUtils.ts deleted file mode 100644 index 6abdb7a..0000000 --- a/frontend/packages/inspector/src/components/chat/messageUtils.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { TimelineEntry } from "./types"; - -export const getMessageClass = (entry: TimelineEntry) => { - if (entry.kind === "tool") return "tool"; - if (entry.kind === "meta") return entry.meta?.severity === "error" ? "error" : "system"; - if (entry.kind === "reasoning") return "assistant"; - if (entry.role === "user") return "user"; - return "assistant"; -}; - -export const getAvatarLabel = (messageClass: string) => { - if (messageClass === "user") return "U"; - if (messageClass === "tool") return "T"; - if (messageClass === "system") return "S"; - if (messageClass === "error") return "!"; - return "AI"; -}; diff --git a/frontend/packages/inspector/src/components/chat/messageUtils.tsx b/frontend/packages/inspector/src/components/chat/messageUtils.tsx new file mode 100644 index 0000000..fba8962 --- /dev/null +++ b/frontend/packages/inspector/src/components/chat/messageUtils.tsx @@ -0,0 +1,20 @@ +import type { UniversalItem } from "sandbox-agent"; +import { Settings, AlertTriangle, User } from "lucide-react"; +import type { ReactNode } from "react"; + +export const getMessageClass = (item: UniversalItem) => { + if (item.kind === "tool_call" || item.kind === "tool_result") return "tool"; + if (item.kind === "system" || item.kind === "status") return "system"; + if (item.role === "user") return "user"; + if (item.role === "tool") return "tool"; + if (item.role === "system") return "system"; + return "assistant"; +}; + +export const getAvatarLabel = (messageClass: string): ReactNode => { + if (messageClass === "user") return ; + if (messageClass === "tool") return "T"; + if (messageClass === "system") return ; + if (messageClass === "error") return ; + return "AI"; +}; diff --git a/frontend/packages/website/astro.config.mjs b/frontend/packages/website/astro.config.mjs index 41e24e0..f11c53c 100644 --- a/frontend/packages/website/astro.config.mjs +++ b/frontend/packages/website/astro.config.mjs @@ -1,11 +1,14 @@ import { defineConfig } from 'astro/config'; import react from '@astrojs/react'; import tailwind from '@astrojs/tailwind'; +import sitemap from '@astrojs/sitemap'; export default defineConfig({ + site: 'https://sandbox-agent.dev', output: 'static', integrations: [ react(), - tailwind() + tailwind(), + sitemap() ] }); diff --git a/frontend/packages/website/package.json b/frontend/packages/website/package.json index aef9c0b..ced76f7 100644 --- a/frontend/packages/website/package.json +++ b/frontend/packages/website/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@astrojs/react": "^4.2.0", + "@astrojs/sitemap": "^3.2.0", "@astrojs/tailwind": "^6.0.0", "astro": "^5.1.0", "framer-motion": "^12.0.0", diff --git a/frontend/packages/website/src/components/CTASection.tsx b/frontend/packages/website/src/components/CTASection.tsx deleted file mode 100644 index f47f14e..0000000 --- a/frontend/packages/website/src/components/CTASection.tsx +++ /dev/null @@ -1,112 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { ArrowRight, Terminal, Check } from 'lucide-react'; - -const CTA_TITLES = [ - 'Run coding agents in sandboxes. Control them over HTTP.', - 'A server inside your sandbox. An API for your app.', - 'Claude Code, Codex, OpenCode, Amp, Pi — one HTTP API.', - 'Your app connects remotely. The coding agent runs isolated.', - 'Streaming events. Handling permissions. Managing sessions.', - 'Install with curl. Connect over HTTP. Control any coding agent.', - 'The bridge between your app and sandboxed coding agents.', -]; - -function AnimatedCTATitle() { - const [currentIndex, setCurrentIndex] = useState(0); - - useEffect(() => { - const interval = setInterval(() => { - setCurrentIndex(prev => (prev + 1) % CTA_TITLES.length); - }, 3000); - - return () => clearInterval(interval); - }, []); - - return ( -

- - - {CTA_TITLES[currentIndex]} - - -

- ); -} - -const CopyInstallButton = () => { - const [copied, setCopied] = useState(false); - const installCommand = 'curl -sSL https://sandboxagent.dev/install | sh'; - - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(installCommand); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (err) { - console.error('Failed to copy:', err); - } - }; - - return ( - - ); -}; - -export function CTASection() { - return ( -
- -
-
- -
- - A server that runs inside isolated environments.
- Your app connects remotely to control any coding agent. -
- - - Read the Docs - - - - -
-
- ); -} diff --git a/frontend/packages/website/src/components/FAQ.tsx b/frontend/packages/website/src/components/FAQ.tsx index 4000c22..fc80332 100644 --- a/frontend/packages/website/src/components/FAQ.tsx +++ b/frontend/packages/website/src/components/FAQ.tsx @@ -61,14 +61,14 @@ function FAQItem({ question, answer }: { question: string; answer: string }) { const [isOpen, setIsOpen] = useState(false); return ( -
+
+
+ +
+ Give this to your coding agent +
+
); }; export function Hero() { + const [scrollOpacity, setScrollOpacity] = useState(1); + + useEffect(() => { + const handleScroll = () => { + const scrollY = window.scrollY; + const windowHeight = window.innerHeight; + const isMobile = window.innerWidth < 1024; + + const fadeStart = windowHeight * (isMobile ? 0.3 : 0.15); + const fadeEnd = windowHeight * (isMobile ? 0.7 : 0.5); + const opacity = 1 - Math.min(1, Math.max(0, (scrollY - fadeStart) / (fadeEnd - fadeStart))); + setScrollOpacity(opacity); + }; + + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + return ( -
-
-
-
-

- Run Coding Agents in Sandboxes.
- Control Them Over HTTP. -

-

- The Sandbox Agent SDK is a server that runs inside your sandbox. Your app connects remotely to control Claude Code, Codex, OpenCode, or Amp — streaming events, handling permissions, managing sessions. -

+
+ {/* Background gradient */} +
-
- +
+ + Run Coding Agents in Sandboxes. +
+ Control Them Over HTTP. + -
- + + The Sandbox Agent SDK is a server that runs inside your sandbox. Your app connects remotely to control Claude Code, Codex, OpenCode, Amp, or Pi — streaming events, handling permissions, managing sessions. + + + + + Read the Docs + + + + +
+ + {/* Right side - Diagram */} + + +
+
); } diff --git a/frontend/packages/website/src/components/Inspector.tsx b/frontend/packages/website/src/components/Inspector.tsx index 7e29185..26cdf09 100644 --- a/frontend/packages/website/src/components/Inspector.tsx +++ b/frontend/packages/website/src/components/Inspector.tsx @@ -1,23 +1,45 @@ 'use client'; +import { motion } from 'framer-motion'; + export function Inspector() { return ( -
-
-

- Built-in Debugger -

-

- Inspect sessions, view event payloads, and troubleshoot without writing code. -

+
+
+
+ + Built-in Debugger + + + Inspect sessions, view event payloads, and troubleshoot without writing code. + +
-
+ Sandbox Agent Inspector -
+
); diff --git a/frontend/packages/website/src/components/Navigation.tsx b/frontend/packages/website/src/components/Navigation.tsx index cc970a4..52c546f 100644 --- a/frontend/packages/website/src/components/Navigation.tsx +++ b/frontend/packages/website/src/components/Navigation.tsx @@ -6,11 +6,12 @@ import { GitHubStars } from './GitHubStars'; function NavItem({ href, children }: { href: string; children: React.ReactNode }) { return ( - + + {children} + ); } @@ -34,15 +35,17 @@ export function Navigation() { isScrolled ? "before:border-white/10" : "before:border-transparent" }`} > + {/* Background with blur */} {/* Desktop Nav */} -
+
Docs Changelog
{/* Right side */} -
+
setMobileMenuOpen(!mobileMenuOpen)} > {mobileMenuOpen ? : } @@ -101,26 +104,26 @@ export function Navigation() { {/* Mobile menu */} {mobileMenuOpen && ( -
-
+
+
setMobileMenuOpen(false)} > Docs setMobileMenuOpen(false)} > Changelog -
+ diff --git a/frontend/packages/website/src/components/PainPoints.tsx b/frontend/packages/website/src/components/PainPoints.tsx index 654286b..e206895 100644 --- a/frontend/packages/website/src/components/PainPoints.tsx +++ b/frontend/packages/website/src/components/PainPoints.tsx @@ -1,133 +1,91 @@ 'use client'; import { motion } from 'framer-motion'; -import { X, Check } from 'lucide-react'; +import { Shield, Layers, Database, X, Check } from 'lucide-react'; const frictions = [ { - number: '01', + icon: Shield, title: 'Coding Agents Need Sandboxes', problem: "You can't let AI execute arbitrary code on your production servers. Coding agents need isolated environments, but existing SDKs assume local execution.", solution: 'A server that runs inside the sandbox and exposes HTTP/SSE.', - accentColor: 'orange', }, { - number: '02', + icon: Layers, title: 'Every Coding Agent is Different', problem: 'Claude Code, Codex, OpenCode, Amp, and Pi each have proprietary APIs, event formats, and behaviors. Swapping coding agents means rewriting your entire integration.', solution: 'One HTTP API. Write your code once, swap coding agents with a config change.', - accentColor: 'purple', }, { - number: '03', + icon: Database, title: 'Sessions Are Ephemeral', problem: 'Coding agent transcripts live in the sandbox. When the process ends, you lose everything. Debugging and replay become impossible.', solution: 'Universal event schema streams to your storage. Persist to Postgres or Rivet, replay later, audit everything.', - accentColor: 'blue', }, ]; -const accentStyles = { - orange: { - gradient: 'from-orange-500/20', - border: 'border-orange-500/30', - glow: 'rgba(255,79,0,0.15)', - number: 'text-orange-500', - }, - purple: { - gradient: 'from-purple-500/20', - border: 'border-purple-500/30', - glow: 'rgba(168,85,247,0.15)', - number: 'text-purple-500', - }, - blue: { - gradient: 'from-blue-500/20', - border: 'border-blue-500/30', - glow: 'rgba(59,130,246,0.15)', - number: 'text-blue-500', - }, -}; - export function PainPoints() { return ( -
+
+
+ + Running coding agents remotely is hard. + + + The Sandbox Agent SDK is a server that runs inside your sandbox. Your app connects remotely to control Claude Code, Codex, OpenCode, Amp, or Pi — streaming events, handling permissions, managing sessions. + +
+ -

- Running coding agents remotely is hard. -

-

- Coding agents need sandboxes, but existing SDKs assume local execution. SSH breaks, CLI wrappers are fragile, and building from scratch means reimplementing everything for each coding agent. -

-
- -
- {frictions.map((friction, index) => { - const styles = accentStyles[friction.accentColor as keyof typeof accentStyles]; - return ( - - {/* Top shine */} -
- - {/* Hover glow */} -
- - {/* Corner highlight */} -
- -
- {/* Title */} -

{friction.title}

- - {/* Problem */} -
-
-
- -
- Problem -
-

{friction.problem}

-
- - {/* Solution */} -
-
-
- -
- Solution -
-

{friction.solution}

-
+ {frictions.map((friction) => ( +
+
+ +
+

{friction.title}

+
+
+ + Problem
- - ); - })} -
+

+ {friction.problem} +

+
+
+
+ + Solution +
+

+ {friction.solution} +

+
+
+ ))} +
); diff --git a/frontend/packages/website/src/layouts/Layout.astro b/frontend/packages/website/src/layouts/Layout.astro index 8e7f8e6..25683cd 100644 --- a/frontend/packages/website/src/layouts/Layout.astro +++ b/frontend/packages/website/src/layouts/Layout.astro @@ -4,32 +4,81 @@ interface Props { description?: string; } -const { title, description = "Universal SDK for coding agents. Control Claude Code, Codex, OpenCode, and Amp with unified events and sessions." } = Astro.props; +const { title, description = "Universal SDK for coding agents. Control Claude Code, Codex, OpenCode, Amp, and Pi with unified events and sessions." } = Astro.props; +const canonicalURL = new URL(Astro.url.pathname, 'https://sandbox-agent.dev'); +const ogImageURL = new URL('/og.png', 'https://sandbox-agent.dev'); + +const structuredData = { + "@context": "https://schema.org", + "@type": "SoftwareApplication", + "name": "Sandbox Agent SDK", + "applicationCategory": "DeveloperApplication", + "operatingSystem": "Linux, macOS, Windows", + "description": description, + "url": "https://sandbox-agent.dev", + "author": { + "@type": "Organization", + "name": "Rivet", + "url": "https://rivet.dev" + }, + "offers": { + "@type": "Offer", + "price": "0", + "priceCurrency": "USD" + }, + "keywords": "coding agents, AI SDK, Claude Code, Codex, OpenCode, Amp, sandbox, remote code execution, developer tools" +}; --- - + + + + + + + + + + + + + + + {title} + - + + + + + - + + + + + + + +