Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
23 changes: 23 additions & 0 deletions src/app/dashboard/[teamSlug]/agents/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Metadata } from 'next'
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',
}

export default function AgentsPage() {
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
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)}>
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[]
7 changes: 6 additions & 1 deletion src/features/dashboard/sandbox/terminal/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 @@ -31,8 +35,9 @@ export default function SandboxTerminalView({
} = useSandboxContext()
const sandboxTemplateId = sandboxInfo?.templateID
const launchTarget = {
command,
sandboxId,
template: sandboxTemplateId,
template: template ?? sandboxTemplateId,
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
}
Comment thread
matthewlouisbrockman marked this conversation as resolved.
Outdated

const finishSandboxResume = async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'

import { useEffect, useId, useState } from 'react'
import { Button } from '@/ui/primitives/button'
import {
Dialog,
Expand All @@ -10,19 +11,28 @@ import {
DialogTitle,
} from '@/ui/primitives/dialog'
import { WarningIcon } from '@/ui/primitives/icons'
import { Textarea } from '@/ui/primitives/textarea'
import type { PendingTerminalLaunch } from './types'

interface DashboardTerminalCommandDialogProps {
launch: PendingTerminalLaunch | null
onCancel: () => void
onConfirm: () => void
onConfirm: (command: string) => void
}

export default function DashboardTerminalCommandDialog({
launch,
onCancel,
onConfirm,
}: DashboardTerminalCommandDialogProps) {
const commandInputId = useId()
const [command, setCommand] = useState('')
const normalizedCommand = command.trim()

useEffect(() => {
setCommand(launch?.command ?? '')
}, [launch])

return (
<Dialog open={!!launch} onOpenChange={(open) => !open && onCancel()}>
<DialogContent hideClose className="sm:max-w-xl">
Expand Down Expand Up @@ -54,10 +64,19 @@ export default function DashboardTerminalCommandDialog({
</code>
</div>
<div className="space-y-1">
<p className="prose-label text-fg-tertiary">Command</p>
<pre className="max-h-48 overflow-auto whitespace-pre-wrap break-words border bg-bg p-3 font-mono text-xs text-fg">
<code>{launch.command}</code>
</pre>
<label
className="prose-label text-fg-tertiary"
htmlFor={commandInputId}
>
Command
</label>
<Textarea
className="max-h-48 min-h-24 font-mono text-xs"
id={commandInputId}
onChange={(event) => setCommand(event.target.value)}
spellCheck={false}
value={command}
/>
</div>
</div>
) : null}
Expand All @@ -66,7 +85,11 @@ export default function DashboardTerminalCommandDialog({
<Button type="button" variant="secondary" onClick={onCancel}>
Cancel
</Button>
<Button type="button" onClick={onConfirm}>
<Button
type="button"
disabled={!normalizedCommand}
onClick={() => onConfirm(normalizedCommand)}
>
Run command
</Button>
</DialogFooter>
Expand Down
Loading
Loading