From 81aeb6e5e5a18e667f8b464b5e1bdcc512c5af49 Mon Sep 17 00:00:00 2001 From: Matt Brockman Date: Mon, 29 Jun 2026 18:30:43 -0700 Subject: [PATCH 1/9] Allow dashboard terminal PTY settings --- .../sandboxes/[sandboxId]/terminal/page.tsx | 19 ++- .../dashboard/[teamSlug]/terminal/page.tsx | 14 +- src/app/dashboard/route.ts | 5 +- src/core/shared/create-envd-sandbox.ts | 24 ++- .../dashboard/sandbox/terminal/view.tsx | 10 +- src/features/dashboard/terminal/constants.ts | 2 +- .../dashboard-terminal-command-dialog.tsx | 78 ++++++--- .../dashboard/terminal/dashboard-terminal.tsx | 124 ++++++++++++-- .../dashboard/terminal/pty-options.ts | 86 ++++++++++ .../terminal/pty-settings-dialog.tsx | 155 ++++++++++++++++++ src/features/dashboard/terminal/storage.ts | 62 ++++++- .../dashboard/terminal/terminal-panel.tsx | 30 +++- src/features/dashboard/terminal/types.ts | 5 +- tests/unit/dashboard-terminal.test.ts | 58 +++++++ tests/unit/sandbox-side-effects.test.ts | 27 +++ 15 files changed, 652 insertions(+), 47 deletions(-) create mode 100644 src/features/dashboard/terminal/pty-options.ts create mode 100644 src/features/dashboard/terminal/pty-settings-dialog.tsx diff --git a/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/terminal/page.tsx b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/terminal/page.tsx index 4d7f78f1a..81eaa323c 100644 --- a/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/terminal/page.tsx +++ b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/terminal/page.tsx @@ -2,6 +2,10 @@ import { redirect } from 'next/navigation' 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 { + hasPtyOptionsSearchParams, + parsePtyOptionsFromSearchParams, +} from '@/features/dashboard/terminal/pty-options' import SandboxTerminalView from '@/features/dashboard/sandbox/terminal/view' interface SandboxTerminalPageProps { @@ -10,6 +14,9 @@ interface SandboxTerminalPageProps { }> searchParams: Promise<{ command?: string + cwd?: string + env?: string | string[] + user?: string }> } @@ -17,11 +24,12 @@ 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) @@ -35,5 +43,12 @@ export default async function SandboxTerminalPage({ redirect(PROTECTED_URLS.DASHBOARD) } - return + return ( + + ) } diff --git a/src/app/dashboard/[teamSlug]/terminal/page.tsx b/src/app/dashboard/[teamSlug]/terminal/page.tsx index 3054eddf1..040dd553d 100644 --- a/src/app/dashboard/[teamSlug]/terminal/page.tsx +++ b/src/app/dashboard/[teamSlug]/terminal/page.tsx @@ -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' @@ -22,8 +26,11 @@ interface TeamTerminalPageProps { }> searchParams: Promise<{ command?: string + cwd?: string + env?: string | string[] sandboxId?: string template?: string + user?: string }> } @@ -31,13 +38,16 @@ export default async function TeamTerminalPage({ params, searchParams, }: TeamTerminalPageProps) { - const [{ teamSlug }, { command = '', sandboxId, template }] = + 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 @@ -106,6 +116,8 @@ export default async function TeamTerminalPage({ launchTarget={{ command, forceNewSandbox: !terminalSandboxId && hasTemplateOverride, + ptyOptions, + requiresConfirmation, sandboxId: terminalSandboxId, template: terminalSandboxId ? terminalTemplate diff --git a/src/app/dashboard/route.ts b/src/app/dashboard/route.ts index a450bb833..d437eadfc 100644 --- a/src/app/dashboard/route.ts +++ b/src/app/dashboard/route.ts @@ -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) diff --git a/src/core/shared/create-envd-sandbox.ts b/src/core/shared/create-envd-sandbox.ts index 7699b5870..6507e6515 100644 --- a/src/core/shared/create-envd-sandbox.ts +++ b/src/core/shared/create-envd-sandbox.ts @@ -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 @@ -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 + } +} diff --git a/src/features/dashboard/sandbox/terminal/view.tsx b/src/features/dashboard/sandbox/terminal/view.tsx index 4b7292377..7b65fb90d 100644 --- a/src/features/dashboard/sandbox/terminal/view.tsx +++ b/src/features/dashboard/sandbox/terminal/view.tsx @@ -3,6 +3,7 @@ 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' @@ -10,6 +11,8 @@ import SandboxInspectNotFound from '../inspect/not-found' interface SandboxTerminalViewProps { command?: string + ptyOptions?: TerminalPtyOptions + requiresConfirmation?: boolean userId: string } @@ -17,6 +20,8 @@ const SANDBOX_TERMINAL_RESUME_TIMEOUT_MS = 70_000 export default function SandboxTerminalView({ command, + ptyOptions, + requiresConfirmation = false, userId, }: SandboxTerminalViewProps) { const [shouldResumeSandbox, setShouldResumeSandbox] = useState(false) @@ -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 () => { @@ -84,6 +91,7 @@ export default function SandboxTerminalView({ return (
{ diff --git a/src/features/dashboard/terminal/constants.ts b/src/features/dashboard/terminal/constants.ts index 6382dc5c2..4d3b39871 100644 --- a/src/features/dashboard/terminal/constants.ts +++ b/src/features/dashboard/terminal/constants.ts @@ -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_STORAGE_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 diff --git a/src/features/dashboard/terminal/dashboard-terminal-command-dialog.tsx b/src/features/dashboard/terminal/dashboard-terminal-command-dialog.tsx index 34c6e902d..3452549fd 100644 --- a/src/features/dashboard/terminal/dashboard-terminal-command-dialog.tsx +++ b/src/features/dashboard/terminal/dashboard-terminal-command-dialog.tsx @@ -17,7 +17,7 @@ import type { PendingTerminalLaunch } from './types' interface DashboardTerminalCommandDialogProps { launch: PendingTerminalLaunch | null onCancel: () => void - onConfirm: (command: string) => void + onConfirm: (command?: string) => void } export default function DashboardTerminalCommandDialog({ @@ -27,6 +27,7 @@ export default function DashboardTerminalCommandDialog({ }: DashboardTerminalCommandDialogProps) { const commandInputId = useId() const [command, setCommand] = useState('') + const hasCommand = !!launch?.command?.trim() const normalizedCommand = command.trim() useEffect(() => { @@ -40,10 +41,13 @@ export default function DashboardTerminalCommandDialog({
- Review terminal command + + {hasCommand ? 'Review terminal command' : 'Review terminal launch'} + - This command will run inside a persistent E2B sandbox after the - terminal opens. + {hasCommand + ? 'This command will run inside a persistent E2B sandbox after the terminal opens.' + : 'This terminal will open with the connection settings from the URL.'} @@ -63,21 +67,51 @@ export default function DashboardTerminalCommandDialog({ {launch.target?.template ?? 'base'}
-
- -