Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
181cf3f
Add prototype agents launcher
matthewlouisbrockman Jun 17, 2026
1746cfa
Make terminal launch command editable
matthewlouisbrockman Jun 17, 2026
c3f4da6
Clean up agent naming
matthewlouisbrockman Jun 17, 2026
96116bb
Merge remote-tracking branch 'origin/main' into feat/prototype-agents
matthewlouisbrockman Jun 17, 2026
164b1ba
Remove alternate agent names
matthewlouisbrockman Jun 17, 2026
1720fce
Stabilize sandbox terminal launch target
matthewlouisbrockman Jun 17, 2026
b3d28e0
Add agents page layout config
matthewlouisbrockman Jun 17, 2026
8e469c1
refactor: migrate feature flags to LaunchDarkly
ben-fornefeld Jun 18, 2026
0e26b79
chore: remove PostHog build vars from app env schema
ben-fornefeld Jun 18, 2026
49437d5
chore: keep PostHog build vars in env schema
ben-fornefeld Jun 18, 2026
f9197d8
fix: cache failed LaunchDarkly initialization
ben-fornefeld Jun 18, 2026
6ae9ccd
Merge remote-tracking branch 'origin/pr-434' into feat/prototype-agents
matthewlouisbrockman Jun 18, 2026
dcf6d2d
Gate agents page behind feature flag
matthewlouisbrockman Jun 18, 2026
b284782
Use underscored agents feature flag key
matthewlouisbrockman Jun 18, 2026
1c387c8
Merge main into prototype agents
matthewlouisbrockman Jun 18, 2026
a8a81dd
Disable prefetch for agent start links
matthewlouisbrockman Jun 18, 2026
af1f19f
Merge remote-tracking branch 'origin/main' into feat/prototype-agents
matthewlouisbrockman Jun 18, 2026
88533f0
Merge remote-tracking branch 'origin/main' into feat/prototype-agents
matthewlouisbrockman Jun 18, 2026
225fd43
Use sandbox template identity for terminal launch
matthewlouisbrockman Jun 19, 2026
119c72d
Use sandbox template for terminal sandbox links
matthewlouisbrockman Jun 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src/app/dashboard/[teamSlug]/agents/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Metadata } from 'next'
import { notFound, redirect } from 'next/navigation'
import { AUTH_URLS } from '@/configs/urls'
import { featureFlags } from '@/core/modules/feature-flags/feature-flags.server'
import { getAuthContext } from '@/core/server/auth'
import { getTeamIdFromSlug } from '@/core/server/functions/team/get-team-id-from-slug'
import { AgentsList } from '@/features/dashboard/agents/agents-list'
import { AGENT_TEMPLATES } from '@/features/dashboard/agents/config'
import { Page } from '@/features/dashboard/layouts/page'

export const metadata: Metadata = {
title: 'Agents - E2B',
}

type AgentsPageProps = {
params: Promise<{
teamSlug: string
}>
}

export default async function AgentsPage({ params }: AgentsPageProps) {
const [{ teamSlug }, authContext] = await Promise.all([
params,
getAuthContext(),
])

if (!authContext) {
redirect(AUTH_URLS.SIGN_IN)
}

const teamId = await getTeamIdFromSlug(teamSlug, authContext.accessToken)

if (!teamId.ok || !teamId.data) {
notFound()
}

const agentsEnabled = await featureFlags.isEnabled('agentsEnabled', {
user: {
id: authContext.user.id,
email: authContext.user.email ?? undefined,
},
team: {
id: teamId.data,
slug: teamSlug,
},
})

if (!agentsEnabled) {
notFound()
}

return (
<Page className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<h2 className="prose-title text-fg">Agents</h2>
<p className="prose-body text-fg-tertiary max-w-2xl">
Start a coding-agent sandbox and open it in the dashboard terminal.
</p>
</div>

<AgentsList agents={AGENT_TEMPLATES} />
</Page>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ interface SandboxTerminalPageProps {
params: Promise<{
teamSlug: string
}>
searchParams: Promise<{
command?: string
template?: string
}>
}

export default async function SandboxTerminalPage({
params,
searchParams,
}: SandboxTerminalPageProps) {
const [{ teamSlug }, authContext] = await Promise.all([
params,
getAuthContext(),
])
const [{ teamSlug }, { command = '', template }, authContext] =
await Promise.all([params, searchParams, getAuthContext()])

if (!authContext) {
redirect(AUTH_URLS.SIGN_IN)
Expand All @@ -33,10 +36,12 @@ export default async function SandboxTerminalPage({

return (
<SandboxTerminalView
command={command}
sandboxManagementAuth={createSandboxManagementAuth(
authContext,
teamId.data
)}
template={template}
Comment thread
matthewlouisbrockman marked this conversation as resolved.
Outdated
/>
)
}
25 changes: 22 additions & 3 deletions src/app/sbx/new/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,24 @@ import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls'
import { getAuthContext } from '@/core/server/auth'
import { resolveUserTeam } from '@/core/server/functions/team/resolve-user-team'
import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger'
import { normalizeTerminalTemplate } from '@/features/dashboard/terminal/template'

export const GET = async (req: NextRequest) => {
try {
const requestUrl = new URL(req.url)
const template = normalizeTerminalTemplate(
requestUrl.searchParams.get('template') ?? undefined
)

if (!template) {
return NextResponse.redirect(new URL(req.url).origin)
}

const authContext = await getAuthContext()

if (!authContext) {
const params = new URLSearchParams({
returnTo: new URL(req.url).pathname,
returnTo: `${requestUrl.pathname}${requestUrl.search}`,
})

return NextResponse.redirect(
Expand All @@ -29,20 +39,29 @@ export const GET = async (req: NextRequest) => {
return NextResponse.redirect(new URL(req.url).origin)
}

const sbx = await Sandbox.create('base', {
const sbx = await Sandbox.create(template, {
apiUrl: process.env.NEXT_PUBLIC_INFRA_API_URL,
domain: process.env.NEXT_PUBLIC_E2B_DOMAIN,
apiHeaders: {
...authHeaders(authContext.accessToken, team.id),
},
})

const terminalParams = new URLSearchParams({ template })
const command = requestUrl.searchParams.get('command')?.trim()

if (command) {
terminalParams.set('command', command)
}

const terminalUrl = PROTECTED_URLS.SANDBOX_TERMINAL(
team.slug,
sbx.sandboxId
)

return NextResponse.redirect(new URL(terminalUrl, req.url))
return NextResponse.redirect(
new URL(`${terminalUrl}?${terminalParams.toString()}`, req.url)
)
} catch (error) {
l.warn(
{
Expand Down
4 changes: 4 additions & 0 deletions src/configs/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
}),
'/dashboard/*/sandboxes/*/*': (pathname) => {
const parts = pathname.split('/')
const teamSlug = parts[2]!

Check warning on line 41 in src/configs/layout.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.
const sandboxId = parts[4]!

Check warning on line 42 in src/configs/layout.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.

return {
title: [
Expand Down Expand Up @@ -70,8 +70,8 @@
}),
'/dashboard/*/templates/*/builds/*': (pathname) => {
const parts = pathname.split('/')
const teamSlug = parts[2]!

Check warning on line 73 in src/configs/layout.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.
const buildId = parts.pop()!

Check warning on line 74 in src/configs/layout.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.
const buildIdSliced = `${buildId.slice(0, 6)}...${buildId.slice(-6)}`

return {
Expand Down Expand Up @@ -118,6 +118,10 @@
title: 'Members',
type: 'default',
}),
'/dashboard/*/agents': () => ({
title: 'Agents',
type: 'default',
}),

// billing
'/dashboard/*/usage': () => ({
Expand All @@ -137,7 +141,7 @@
}),
'/dashboard/*/billing/plan': (pathname) => {
const parts = pathname.split('/')
const teamSlug = parts[2]!

Check warning on line 144 in src/configs/layout.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.

return {
title: [
Expand All @@ -151,7 +155,7 @@
},
'/dashboard/*/billing/plan/select': (pathname) => {
const parts = pathname.split('/')
const teamSlug = parts[2]!

Check warning on line 158 in src/configs/layout.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.

return {
title: [
Expand All @@ -175,8 +179,8 @@
// Pathname fallback for detail tabs; usePageTitle replaces with the friendly template name once data loads.
function templateDetailLayoutConfig(pathname: string): DashboardLayoutConfig {
const parts = pathname.split('/')
const teamSlug = parts[2]!

Check warning on line 182 in src/configs/layout.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.
const templateId = parts[4]!

Check warning on line 183 in src/configs/layout.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.
const templateIdSliced =
templateId.length > 14
? `${templateId.slice(0, 6)}...${templateId.slice(-6)}`
Expand Down
7 changes: 7 additions & 0 deletions src/core/modules/feature-flags/definitions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import type { FeatureFlagDefinition } from '@/core/modules/feature-flags/types'

export const FEATURE_FLAGS = {
agentsEnabled: {
kind: 'boolean',
key: 'agents_enabled',
defaultValue: false,
description: 'Enables the dashboard agents launcher.',
exposure: 'server',
},
isAdmin: {
kind: 'boolean',
key: 'is_admin',
Expand Down
76 changes: 76 additions & 0 deletions src/features/dashboard/agents/agents-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use client'

import Link from 'next/link'
import type { ComponentType } from 'react'
import { SiClaude, SiOpenai } from 'react-icons/si'
import { cn } from '@/lib/utils'
import { Button } from '@/ui/primitives/button'
import { ExternalLinkIcon, UnpackIcon } from '@/ui/primitives/icons'
import type { AgentTemplateConfig } from './config'

const AGENT_ICONS = {
claude: SiClaude,
open: UnpackIcon,
openai: SiOpenai,
} satisfies Record<
AgentTemplateConfig['icon'],
ComponentType<{ className?: string }>
>

function getLaunchHref(agent: AgentTemplateConfig) {
const params = new URLSearchParams({
command: agent.command,
template: agent.template,
})

return `/sbx/new?${params.toString()}`
Comment thread
matthewlouisbrockman marked this conversation as resolved.
}

export function AgentsList({
agents,
className,
}: {
agents: AgentTemplateConfig[]
className?: string
}) {
return (
<div className={cn('grid gap-3 sm:grid-cols-2 xl:grid-cols-3', className)}>
{agents.map((agent) => (
<AgentCard agent={agent} key={agent.id} />
))}
</div>
)
}

function AgentCard({ agent }: { agent: AgentTemplateConfig }) {
const AgentIcon = AGENT_ICONS[agent.icon]

return (
<section className="border-stroke bg-bg-1 flex min-h-44 flex-col rounded-lg border p-4">
<div className="flex min-w-0 items-start gap-3">
<div className="border-stroke bg-bg flex size-9 shrink-0 items-center justify-center rounded-md border">
<AgentIcon className="size-4" />
</div>
<div className="min-w-0">
<h3 className="prose-body-highlight text-fg truncate">
{agent.name}
</h3>
<p className="prose-body text-fg-tertiary mt-1 line-clamp-2">
{agent.description}
</p>
</div>
</div>

<div className="prose-label text-fg-tertiary mt-auto pt-4 uppercase">
{agent.template}
</div>

<Button asChild className="mt-3 w-full" variant="primary">
<Link href={getLaunchHref(agent)} prefetch={false}>
Start
<ExternalLinkIcon />
</Link>
Comment thread
cursor[bot] marked this conversation as resolved.
</Button>
</section>
)
}
35 changes: 35 additions & 0 deletions src/features/dashboard/agents/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export type AgentTemplateConfig = {
id: string
name: string
command: string
template: string
icon: 'claude' | 'open' | 'openai'
description: string
}

export const AGENT_TEMPLATES = [
{
id: 'codex',
name: 'Codex',
command: 'codex',
template: 'codex',
icon: 'openai',
description: 'Codex CLI for coding sessions.',
},
{
id: 'claude',
name: 'Claude',
command: 'claude',
template: 'claude',
icon: 'claude',
description: 'Claude Code for coding sessions.',
},
{
id: 'opencode',
name: 'OpenCode',
command: 'opencode',
template: 'opencode',
icon: 'open',
description: 'OpenCode for coding sessions.',
},
] satisfies AgentTemplateConfig[]
18 changes: 13 additions & 5 deletions src/features/dashboard/sandbox/terminal/view.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { type ReactNode, useState } from 'react'
import { type ReactNode, useMemo, useState } from 'react'
import type { SandboxManagementAuth } from '@/core/shared/sandbox-management-auth'
import LoadingLayout from '@/features/dashboard/loading-layout'
import DashboardTerminal from '@/features/dashboard/terminal/dashboard-terminal'
Expand All @@ -10,13 +10,17 @@ import { useSandboxContext } from '../context'
import SandboxInspectNotFound from '../inspect/not-found'

interface SandboxTerminalViewProps {
command?: string
sandboxManagementAuth: SandboxManagementAuth
template?: string
}

const SANDBOX_TERMINAL_RESUME_TIMEOUT_MS = 70_000

export default function SandboxTerminalView({
command,
sandboxManagementAuth,
template,
}: SandboxTerminalViewProps) {
const [shouldResumeSandbox, setShouldResumeSandbox] = useState(false)
const [terminalResumeError, setTerminalResumeError] = useState<string>()
Expand All @@ -30,10 +34,14 @@ export default function SandboxTerminalView({
refetchSandboxInfo,
} = useSandboxContext()
const sandboxTemplateId = sandboxInfo?.templateID
const launchTarget = {
sandboxId,
template: sandboxTemplateId,
}
const launchTarget = useMemo(
() => ({
command,
sandboxId,
template: template ?? sandboxTemplateId,
}),
Comment thread
cursor[bot] marked this conversation as resolved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: please use sandboxInfo.alias ?? .templateID here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kk, fixed both here and in the display

[command, sandboxId, sandboxTemplateId, template]
)

const finishSandboxResume = async () => {
const nextSandboxInfo = await refetchSandboxInfo()
Expand Down
Loading
Loading