Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,33 @@ import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls'
import { getAuthContext } from '@/core/server/auth'
import { getTeamIdFromSlug } from '@/core/server/functions/team/get-team-id-from-slug'
import SandboxTerminalView from '@/features/dashboard/sandbox/terminal/view'
import {
hasPtyOptionsSearchParams,
parsePtyOptionsFromSearchParams,
} from '@/features/dashboard/terminal/pty-options'

interface SandboxTerminalPageProps {
params: Promise<{
teamSlug: string
}>
searchParams: Promise<{
command?: string
cwd?: string
env?: string | string[]
user?: string
}>
}

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

if (!authContext) {
redirect(AUTH_URLS.SIGN_IN)
Expand All @@ -35,5 +43,12 @@ export default async function SandboxTerminalPage({
redirect(PROTECTED_URLS.DASHBOARD)
}

return <SandboxTerminalView command={command} userId={authContext.user.id} />
return (
<SandboxTerminalView
command={command}
ptyOptions={parsePtyOptionsFromSearchParams(terminalSearchParams)}
requiresConfirmation={hasPtyOptionsSearchParams(terminalSearchParams)}
userId={authContext.user.id}
/>
)
}
38 changes: 34 additions & 4 deletions src/app/dashboard/[teamSlug]/terminal/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { infra } from '@/core/shared/clients/api'
import type { components as InfraComponents } from '@/core/shared/contracts/infra-api.types'
import { SandboxIdSchema } from '@/core/shared/schemas/api'
import DashboardTerminal from '@/features/dashboard/terminal/dashboard-terminal'
import {
hasPtyOptionsSearchParams,
parsePtyOptionsFromSearchParams,
} from '@/features/dashboard/terminal/pty-options'
import { normalizeTerminalTemplate } from '@/features/dashboard/terminal/template'
import { Button } from '@/ui/primitives/button'

Expand All @@ -22,22 +26,30 @@ interface TeamTerminalPageProps {
}>
searchParams: Promise<{
command?: string
cwd?: string
env?: string | string[]
sandboxId?: string
template?: string
user?: string
}>
}

export default async function TeamTerminalPage({
params,
searchParams,
}: TeamTerminalPageProps) {
const [{ teamSlug }, { command = '', sandboxId, template }] =
await Promise.all([params, searchParams])
const [{ teamSlug }, terminalSearchParams] = await Promise.all([
params,
searchParams,
])
const { command = '', sandboxId, template } = terminalSearchParams
const hasTemplateOverride = template !== undefined
const requestedTemplate = hasTemplateOverride
? normalizeTerminalTemplate(template)
: undefined
const terminalSandboxId = normalizeTerminalSandboxId(sandboxId)
const ptyOptions = parsePtyOptionsFromSearchParams(terminalSearchParams)
const requiresConfirmation = hasPtyOptionsSearchParams(terminalSearchParams)

if (!terminalSandboxId && hasTemplateOverride && !requestedTemplate) {
return <TerminalUnavailable message="The terminal template is invalid." />
Expand All @@ -53,11 +65,14 @@ export default async function TeamTerminalPage({
return (
<TerminalSignIn
command={command}
cwd={terminalSearchParams.cwd}
env={terminalSearchParams.env}
sandboxId={terminalSandboxId}
teamSlug={teamSlug}
template={
terminalSandboxId ? template : (requestedTemplate ?? undefined)
}
user={terminalSearchParams.user}
/>
)
}
Expand Down Expand Up @@ -106,6 +121,8 @@ export default async function TeamTerminalPage({
launchTarget={{
command,
forceNewSandbox: !terminalSandboxId && hasTemplateOverride,
ptyOptions,
requiresConfirmation,
sandboxId: terminalSandboxId,
template: terminalSandboxId
? terminalTemplate
Expand Down Expand Up @@ -157,20 +174,33 @@ async function getSandboxInTeam({

function TerminalSignIn({
command,
cwd,
env,
sandboxId,
teamSlug,
template,
user,
}: {
command?: string
cwd?: string
env?: string | string[]
sandboxId?: string
teamSlug: string
template?: string
user?: string
}) {
const returnToQuery = new URLSearchParams({
const returnToParams = new URLSearchParams({
...(command ? { command } : {}),
...(cwd ? { cwd } : {}),
...(sandboxId ? { sandboxId } : {}),
...(template ? { template } : {}),
}).toString()
...(user ? { user } : {}),
})
const envValues = Array.isArray(env) ? env : env ? [env] : []
for (const value of envValues) {
returnToParams.append('env', value)
}
const returnToQuery = returnToParams.toString()
const returnTo = `${PROTECTED_URLS.TERMINAL(teamSlug)}${
returnToQuery ? `?${returnToQuery}` : ''
}`
Expand Down
5 changes: 4 additions & 1 deletion src/app/dashboard/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,16 @@ export async function GET(request: NextRequest) {

// send everything to terminal if it's terminal
if (tab === 'terminal') {
const terminalParams = ['template', 'sandboxId', 'command']
const terminalParams = ['template', 'sandboxId', 'command', 'user', 'cwd']
terminalParams.forEach((param) => {
const value = searchParams.get(param)
if (value) {
redirectUrl.searchParams.set(param, value)
}
})
searchParams.getAll('env').forEach((value) => {
redirectUrl.searchParams.append('env', value)
})
}

return NextResponse.redirect(redirectUrl)
Expand Down
24 changes: 23 additions & 1 deletion src/core/shared/create-envd-sandbox.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Sandbox, { type SandboxConnectOpts } from 'e2b'

const SDK_ENVD_PORT = 49983

/**
* Sandbox-scoped credentials needed to talk to a sandbox's envd daemon
* (filesystem + PTY). These are safe to expose to the browser: they grant
Expand Down Expand Up @@ -49,6 +51,26 @@ export function createEnvdSandbox(params: EnvdSandboxParams): Sandbox {
envdAccessToken: params.envdAccessToken,
trafficAccessToken: params.trafficAccessToken,
domain: params.domain,
sandboxUrl: params.sandboxUrl,
sandboxUrl: resolveBrowserSandboxUrl(params.sandboxUrl, params.sandboxId),
})
}

export function resolveBrowserSandboxUrl(
sandboxUrl: string | undefined,
sandboxId: string
) {
if (!sandboxUrl || typeof window === 'undefined') return sandboxUrl

try {
const url = new URL(sandboxUrl)
const sandboxHostPrefix = `${SDK_ENVD_PORT}-${sandboxId}.`

if (!url.hostname.startsWith(sandboxHostPrefix)) {
url.hostname = `${sandboxHostPrefix}${url.hostname}`
}

return url.toString()
} catch {
return sandboxUrl
}
}
10 changes: 9 additions & 1 deletion src/features/dashboard/sandbox/terminal/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@
import { type ReactNode, useMemo, useState } from 'react'
import LoadingLayout from '@/features/dashboard/loading-layout'
import DashboardTerminal from '@/features/dashboard/terminal/dashboard-terminal'
import type { TerminalPtyOptions } from '@/features/dashboard/terminal/pty-options'
import { useRouteParams } from '@/lib/hooks/use-route-params'
import { useDashboard } from '../../context'
import { useSandboxContext } from '../context'
import SandboxInspectNotFound from '../inspect/not-found'

interface SandboxTerminalViewProps {
command?: string
ptyOptions?: TerminalPtyOptions
requiresConfirmation?: boolean
userId: string
}

const SANDBOX_TERMINAL_RESUME_TIMEOUT_MS = 70_000

export default function SandboxTerminalView({
command,
ptyOptions,
requiresConfirmation = false,
userId,
}: SandboxTerminalViewProps) {
const [shouldResumeSandbox, setShouldResumeSandbox] = useState(false)
Expand All @@ -34,10 +39,12 @@ export default function SandboxTerminalView({
const launchTarget = useMemo(
() => ({
command,
ptyOptions,
requiresConfirmation,
sandboxId,
template: sandboxTemplate,
}),
[command, sandboxId, sandboxTemplate]
[command, ptyOptions, requiresConfirmation, sandboxId, sandboxTemplate]
)

const finishSandboxResume = async () => {
Expand Down Expand Up @@ -84,6 +91,7 @@ export default function SandboxTerminalView({
return (
<div className="flex min-h-0 flex-1 overflow-hidden p-3 md:p-6">
<DashboardTerminal
allowPtySettings
autoStart
launchTarget={launchTarget}
onSandboxAttached={() => {
Expand Down
2 changes: 1 addition & 1 deletion src/features/dashboard/terminal/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const DEFAULT_COLS = 100
export const DEFAULT_ROWS = 28
export const DEFAULT_PANEL_HEIGHT = 260
export const MAX_TERMINAL_TRANSCRIPT_CHARS = 200_000
export const TERMINAL_PTY_SETTINGS_COOKIE_PREFIX = 'dashboard-terminal-pty'
export const TERMINAL_SESSION_STORAGE_PREFIX = 'dashboard-terminal-session'
export const DEFAULT_CWD = '/home/user'
export const TERMINAL_AUTOSTART_DEBOUNCE_MS = 300
export const TERMINAL_ATTACH_ATTEMPT_TIMEOUT_MS = 15_000
Loading
Loading