Skip to content
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a21aeaf
Share dashboard panel frame
matthewlouisbrockman May 27, 2026
c954aee
Move xterm styles into terminal component
matthewlouisbrockman May 27, 2026
171563e
Render terminal in dashboard panel frame
matthewlouisbrockman May 27, 2026
dc9d1e9
Use type-only terminal SDK imports
matthewlouisbrockman May 27, 2026
7ec924d
Keep sandbox terminals out of stored sessions
matthewlouisbrockman May 27, 2026
ae2b295
Make terminal restart action caller-controlled
matthewlouisbrockman May 27, 2026
e104b6c
Retry timed-out sandbox terminal attaches
matthewlouisbrockman May 27, 2026
0efbfb4
Debounce terminal autostart
matthewlouisbrockman May 27, 2026
0672776
Reset terminal input queue on disconnect
matthewlouisbrockman May 27, 2026
b6af83c
Show missing terminal sandbox access copy
matthewlouisbrockman May 27, 2026
c4257da
Add terminal tab to sandbox details
matthewlouisbrockman May 27, 2026
5fcd614
Add terminal tab route boundaries
matthewlouisbrockman May 27, 2026
9195326
Extract terminal instance hook
matthewlouisbrockman May 27, 2026
e61ed34
Remove sandbox inspect frame shim
matthewlouisbrockman May 27, 2026
2c9d146
Use dashboard panel frame directly
matthewlouisbrockman May 27, 2026
56ae8bf
Keep shared frame named for sandbox inspect
matthewlouisbrockman May 27, 2026
2b38566
Extract terminal attach retry helper
matthewlouisbrockman May 27, 2026
19faacd
Allow zero-delay terminal attach retries
matthewlouisbrockman May 27, 2026
2165839
Use bounded terminal attach backoff
matthewlouisbrockman May 28, 2026
9d6b490
Pass terminal launch options as a target
matthewlouisbrockman May 28, 2026
3c962d0
Resume paused sandbox inspect views for five minutes
matthewlouisbrockman May 28, 2026
2777f17
Use sandbox context in terminal tab
matthewlouisbrockman May 28, 2026
d6f7003
Reset sandbox terminal resume after attach failure
matthewlouisbrockman May 28, 2026
5ce2360
Kill terminal PTYs on teardown
matthewlouisbrockman May 28, 2026
320df06
Batch terminal input writes
matthewlouisbrockman May 28, 2026
3f4a7f7
Use terminal copy for terminal empty state
matthewlouisbrockman May 28, 2026
e1a2b96
Use sandbox context for terminal lifecycle
matthewlouisbrockman May 28, 2026
8d9f1dd
Batch terminal input and split panel header
matthewlouisbrockman May 28, 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
51 changes: 51 additions & 0 deletions src/app/api/sandbox/terminal/pty/kill/route.ts

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.

why does this live on the server side? can we move this to client side? if not please inside the trpc sandbox router

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.

k

Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import 'server-cli-only'

import Sandbox from 'e2b'
import { z } from 'zod'
import { SUPABASE_AUTH_HEADERS } from '@/configs/api'
import { auth } from '@/core/server/auth'
import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger'

const BodySchema = z.object({
pid: z.number().int().positive(),
sandboxId: z.string().min(1),
teamId: z.string().min(1),
})

export async function POST(request: Request) {
try {
const parsedBody = BodySchema.safeParse(await request.json())

if (!parsedBody.success) {
return Response.json({ error: 'Invalid request' }, { status: 400 })
}

const authContext = await auth.getAuthContext()

if (!authContext) {
return Response.json({ error: 'Unauthenticated' }, { status: 401 })
}

const { pid, sandboxId, teamId } = parsedBody.data
const sandbox = await Sandbox.connect(sandboxId, {
domain: process.env.NEXT_PUBLIC_E2B_DOMAIN,
headers: {
...SUPABASE_AUTH_HEADERS(authContext.accessToken, teamId),
},
})

await sandbox.pty.kill(pid)

return Response.json({ ok: true })
} catch (error) {
l.error(
{
key: 'terminal_pty_kill_route:unexpected_error',
error: serializeErrorForLog(error),
},
`${error instanceof Error ? error.message : 'Failed to kill terminal PTY'}`
)

return Response.json({ error: 'Internal server error' }, { status: 500 })
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use client'

import { DashboardRouteError } from '@/features/dashboard/shared/route-error'

export default function SandboxTerminalPageError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return <DashboardRouteError error={error} reset={reset} />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@/features/dashboard/loading-layout'
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import SandboxTerminalView from '@/features/dashboard/sandbox/terminal/view'

export default function SandboxTerminalPage() {
return <SandboxTerminalView />
}
Comment thread
cursor[bot] marked this conversation as resolved.
9 changes: 0 additions & 9 deletions src/app/dashboard/terminal/layout.tsx

This file was deleted.

18 changes: 14 additions & 4 deletions src/app/dashboard/terminal/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,15 @@ export default async function TerminalPage({
: teamsResult.data.find((candidate) => candidate.id === resolvedTeam?.id)

if (!team) {
return <TerminalUnavailable />
return (
<TerminalUnavailable
message={
terminalSandboxId
? 'Sandbox not found or you do not have access to it.'
: undefined
}
/>
)
}

const templateAvailable = terminalSandboxId
Expand Down Expand Up @@ -107,9 +115,11 @@ export default async function TerminalPage({
<main className="h-dvh min-h-[360px] bg-bg p-3">
<DashboardTerminal
autoStart
initialCommand={command}
initialSandboxId={terminalSandboxId}
initialTemplate={terminalTemplate}
launchTarget={{
command,
sandboxId: terminalSandboxId,
template: terminalTemplate,
}}
teamId={team.id}
/>
</main>
Expand Down
2 changes: 2 additions & 0 deletions src/configs/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const PROTECTED_URLS = {
`/dashboard/${teamSlug}/sandboxes/${sandboxId}/events`,
SANDBOX_LOGS: (teamSlug: string, sandboxId: string) =>
`/dashboard/${teamSlug}/sandboxes/${sandboxId}/logs`,
SANDBOX_TERMINAL: (teamSlug: string, sandboxId: string) =>
`/dashboard/${teamSlug}/sandboxes/${sandboxId}/terminal`,
SANDBOX_FILESYSTEM: (teamSlug: string, sandboxId: string) =>
`/dashboard/${teamSlug}/sandboxes/${sandboxId}/filesystem`,

Expand Down
2 changes: 1 addition & 1 deletion src/features/dashboard/sandbox/inspect/filesystem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import LoadingLayout from '@/features/dashboard/loading-layout'
import SandboxInspectFilesystemHeader from '@/features/dashboard/sandbox/inspect/filesystem-header'
import { SandboxInspectFrame } from '@/features/dashboard/shared'
import { ScrollArea } from '@/ui/primitives/scroll-area'
import { useSandboxContext } from '../context'
import SandboxInspectFrame from './frame'
import { useDirectoryState } from './hooks/use-directory'
import { useRootChildren } from './hooks/use-node'
import SandboxInspectNode from './node'
Expand Down
193 changes: 153 additions & 40 deletions src/features/dashboard/sandbox/inspect/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,49 @@
'use client'

import Sandbox from 'e2b'
import { useParams, useRouter } from 'next/navigation'
import type { ReactNode } from 'react'
import { useCallback, useEffect, useState, useTransition } from 'react'
import { PROTECTED_URLS } from '@/configs/urls'
import { SUPABASE_AUTH_HEADERS } from '@/configs/api'
import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls'
import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger'
import { useSandboxInspectAnalytics } from '@/lib/hooks/use-analytics'
import { supabase } from '@/core/shared/clients/supabase/client'
import { useDashboard } from '@/features/dashboard/context'
import { cn } from '@/lib/utils'
import { Button } from '@/ui/primitives/button'
import {
ArrowLeftIcon,
ArrowUpIcon,
HomeIcon,
RefreshIcon,
RunningIcon,
} from '@/ui/primitives/icons'
import { useSandboxContext } from '../context'
import SandboxInspectEmptyFrame from './empty'

export default function SandboxInspectNotFound() {
const SANDBOX_RESUME_TIMEOUT_MS = 5 * 60 * 1000

interface SandboxInspectNotFoundProps {
onResumeSandbox?: () => void
resource?: 'filesystem' | 'terminal'
}

export default function SandboxInspectNotFound({
onResumeSandbox,
resource = 'filesystem',
}: SandboxInspectNotFoundProps) {
const router = useRouter()
const { isRunning } = useSandboxContext()
const { team } = useDashboard()
const { isRunning, sandboxInfo, refetchSandboxInfo } = useSandboxContext()

const { teamSlug } = useParams()

const [pendingPath, setPendingPath] = useState<string | undefined>(undefined)
const [isResumePending, setIsResumePending] = useState(false)
const [isPending, startTransition] = useTransition()
const [isResetPending, resetTransition] = useTransition()

const save = async (newPath: string) => {
const save = useCallback(async (newPath: string) => {
try {
await fetch('/api/sandbox/inspect/root-path', {
method: 'POST',
Expand All @@ -42,7 +59,7 @@ export default function SandboxInspectNotFound() {
`${error instanceof Error ? error.message : 'Failed to save root path'}`
)
}
}
}, [])

const setRootPath = useCallback(
(newPath: string) => {
Expand All @@ -52,41 +69,114 @@ export default function SandboxInspectNotFound() {
router.refresh()
})
},
[router, startTransition]
[router, save]
)

const resumeSandbox = useCallback(async () => {

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.

this should live inside the sandbox context. please check the state management here and implement accordingly

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.

k

if (onResumeSandbox) {
onResumeSandbox()
return
}

if (!sandboxInfo) return

setIsResumePending(true)
try {
const { data } = await supabase.auth.getSession()

if (!data.session) {
router.replace(AUTH_URLS.SIGN_IN)
return
}

await Sandbox.connect(sandboxInfo.sandboxID, {
domain: process.env.NEXT_PUBLIC_E2B_DOMAIN,
timeoutMs: SANDBOX_RESUME_TIMEOUT_MS,
headers: {
...SUPABASE_AUTH_HEADERS(data.session.access_token, team.id),
},
})

await refetchSandboxInfo()
} catch (error) {
l.error(
{
key: 'sandbox_inspect_not_found:resume_failed',
error: serializeErrorForLog(error),
sandbox_id: sandboxInfo.sandboxID,
},
`${error instanceof Error ? error.message : 'Failed to resume sandbox'}`
)
} finally {
setIsResumePending(false)
}
}, [onResumeSandbox, refetchSandboxInfo, router, sandboxInfo, team.id])

useEffect(() => {
if (!isPending) {
setPendingPath(undefined)
}
}, [isPending])

const description = isRunning
? 'This directory appears to be empty or does not exist. You can reset to the default state, navigate to root, or refresh to try again.'
: 'It seems like the sandbox is not connected anymore. We cannot access the filesystem at this time.'
const isPaused = sandboxInfo?.state === 'paused'
const resourceName = resource === 'terminal' ? 'terminal' : 'filesystem'
const isFilesystem = resource === 'filesystem'

const actions = isRunning ? (
<>
<div className="flex w-full justify-between gap-4">
<Button
variant="secondary"
className="flex-1 gap-2"
onClick={() => setRootPath('')}
disabled={isPending && pendingPath === ''}
>
<HomeIcon className="text-fg-tertiary h-4 w-4" />
Reset
</Button>
const description =
isRunning && isFilesystem
? 'This directory appears to be empty or does not exist. You can reset to the default state, navigate to root, or refresh to try again.'
: isRunning
? 'The terminal is unavailable right now. Refresh to try again.'
: isPaused
? `Resume this sandbox to access the ${resourceName}.`
: `It seems like the sandbox is not connected anymore. We cannot access the ${resourceName} at this time.`

let actions: ReactNode

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.

this should be a separate jsx component

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.

k


if (isRunning && isFilesystem) {
actions = (
<>
<div className="flex w-full justify-between gap-4">
<Button
variant="secondary"
className="flex-1 gap-2"
onClick={() => setRootPath('')}
disabled={isPending && pendingPath === ''}
>
<HomeIcon className="text-fg-tertiary h-4 w-4" />
Reset
</Button>
<Button
variant="secondary"
className="flex-1 gap-2"
onClick={() => setRootPath('/')}
disabled={isPending && pendingPath === '/'}
>
<ArrowUpIcon className="text-fg-tertiary h-4 w-4" />
To Root
</Button>
</div>
<Button
variant="secondary"
className="flex-1 gap-2"
onClick={() => setRootPath('/')}
disabled={isPending && pendingPath === '/'}
onClick={() =>
resetTransition(async () => {
router.refresh()
})
}
className="w-full gap-2"
disabled={isResetPending}
>
<ArrowUpIcon className="text-fg-tertiary h-4 w-4" />
To Root
<RefreshIcon
className={cn('text-fg-tertiary h-4 w-4 transition-transform', {
'animate-spin': isResetPending,
})}
/>
Refresh
</Button>
</div>
</>
)
} else if (isRunning) {
actions = (
<Button
variant="secondary"
onClick={() =>
Expand All @@ -104,21 +194,44 @@ export default function SandboxInspectNotFound() {
/>
Refresh
</Button>
</>
) : (
<Button
variant="secondary"
onClick={() => router.push(PROTECTED_URLS.SANDBOXES(teamSlug as string))}
className="w-full gap-2"
>
<ArrowLeftIcon className="text-fg-tertiary h-4 w-4" />
Back to Sandboxes
</Button>
)
)
} else if (isPaused) {
actions = (
<Button
className="w-full gap-2"
onClick={resumeSandbox}
disabled={isResumePending}
>
<RunningIcon className="h-4 w-4" />
Resume sandbox
</Button>
)
} else {
actions = (
<Button
variant="secondary"
onClick={() =>
router.push(PROTECTED_URLS.SANDBOXES(teamSlug as string))
}
className="w-full gap-2"
>
<ArrowLeftIcon className="text-fg-tertiary h-4 w-4" />
Back to Sandboxes
</Button>
)
}

return (
<SandboxInspectEmptyFrame
title={isRunning ? 'Empty Directory' : 'Not Connected'}
title={
isRunning && isFilesystem
? 'Empty Directory'
: isRunning
? 'Terminal Unavailable'
: isPaused
? 'Sandbox Paused'
: 'Not Connected'
}
description={description}
actions={actions}
/>
Expand Down
Loading
Loading