Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
65 changes: 65 additions & 0 deletions actions/sandbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use server'

/**
* Sandbox Server Actions
*
* Server Actions for sandbox operations. Frontend components call these
* instead of API Routes directly.
*
* TODO: Migrate from app/api/sandbox/:
* - app-status (GET/DELETE)
* - exec (POST)
* - cwd (GET/PUT)
*/

import { auth } from '@/lib/auth'
import { getSandboxTtydContext } from '@/lib/util/ttyd-context'
import { execCommand, TtydExecError } from '@/lib/util/ttyd-exec'

import type { ExecResult } from './types'

/**
* Execute a command in the sandbox and wait for output.
*
* @param sandboxId - The sandbox ID
* @param command - The command to execute
* @param timeoutMs - Optional timeout in milliseconds (default: 30000)
*/
export async function runCommand(
sandboxId: string,
command: string,
timeoutMs?: number
): Promise<ExecResult> {
const session = await auth()

if (!session) {
return { success: false, error: 'Unauthorized' }
}

try {
const { ttyd } = await getSandboxTtydContext(sandboxId, session.user.id)
const { baseUrl, accessToken, authorization } = ttyd

const output = await execCommand(baseUrl, accessToken, command, timeoutMs, authorization)

return { success: true, output }
} catch (error) {
console.error('Failed to execute command in sandbox:', error)
const errorMessage = error instanceof TtydExecError ? error.message : 'Unknown error'
return { success: false, error: errorMessage }
}
}

/**
* Execute a command in the sandbox without waiting for output.
*
* @param sandboxId - The sandbox ID
* @param command - The command to execute
*/
export async function runCommandDetached(
_sandboxId: string,
_command: string
): Promise<ExecResult> {
// TODO: Implement detached command execution
throw new Error('Not implemented')
}
File renamed without changes.
13 changes: 13 additions & 0 deletions actions/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Type definitions for Server Actions
*/

// =============================================================================
// Sandbox Actions
// =============================================================================

export type ExecResult = {
success: boolean
output?: string
error?: string
}
2 changes: 1 addition & 1 deletion components/home-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useSession } from 'next-auth/react';

import { authenticateWithSealos } from '@/actions/sealos-auth';
import { MatrixRain } from '@/components/MatrixRain';
import { Button } from '@/components/ui/button';
import { authenticateWithSealos } from '@/lib/actions/sealos-auth';
import { useSealos } from '@/provider/sealos';

/**
Expand Down
2 changes: 1 addition & 1 deletion components/terminal/terminal-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
import { useState } from 'react';
import type { Prisma } from '@prisma/client';

import { type Tab, TerminalToolbar } from './toolbar/toolbar';
import { TerminalDisplay } from './terminal-display';
import { type Tab, TerminalToolbar } from './terminal-toolbar';

// ============================================================================
// Types
Expand Down
91 changes: 91 additions & 0 deletions components/terminal/toolbar/app-runner-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';

interface AppRunnerDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
sandboxUrl: string | null | undefined;
}

export function AppRunnerDialog({
open,
onOpenChange,
onConfirm,
sandboxUrl,
}: AppRunnerDialogProps) {
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent className="bg-[#252526] border-[#3e3e42] text-white">
<AlertDialogHeader>
<AlertDialogTitle>Run Application & Keep Active?</AlertDialogTitle>
<AlertDialogDescription className="text-gray-400 space-y-3" asChild>
<div className="text-sm text-gray-400 space-y-3">
<div>
This will build and start your application by running:
<br />
<code className="bg-[#1e1e1e] px-1.5 py-0.5 rounded text-xs border border-[#3e3e42] mt-1 inline-block font-mono text-blue-400">
pnpm build && pnpm start
</code>
</div>

<div className="bg-[#1e1e1e]/50 rounded-md border border-[#3e3e42]/50 text-sm">
<div className="p-3 space-y-2">
<div className="flex gap-2.5 items-start">
<span className="text-blue-400 mt-0.5">•</span>
<span>App runs continuously in the background</span>
</div>
<div className="flex gap-2.5 items-start">
<span className="text-blue-400 mt-0.5">•</span>
<span>Remains active even if you leave this page</span>
</div>
<div className="flex gap-2.5 items-start">
<span className="text-blue-400 mt-0.5">•</span>
<span>
Can be stopped anytime by clicking this button again
</span>
</div>
</div>

{sandboxUrl && (
<div className="px-3 pb-3 pt-2 border-t border-[#3e3e42]/30">
<div className="text-xs text-gray-500 mb-1">
Once running, your application will be available at:
</div>
<a
href={sandboxUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-[#3794ff] hover:text-[#4fc1ff] break-all underline underline-offset-2 hover:underline-offset-4 transition-all block"
>
{sandboxUrl}
</a>
</div>
)}
</div>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="bg-transparent border-[#3e3e42] text-gray-300 hover:bg-[#37373d] hover:text-white">
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={onConfirm}
className="bg-[#007fd4] hover:bg-[#0060a0] text-white"
>
Confirm & Run
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
97 changes: 97 additions & 0 deletions components/terminal/toolbar/app-runner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use client';

import { useState } from 'react';
import type { Prisma } from '@prisma/client';
import { Loader2, Play, Square } from 'lucide-react';

import { useAppRunner } from '@/hooks/use-app-runner';
import { cn } from '@/lib/utils';

import { AppRunnerDialog } from './app-runner-dialog';
import { DirectorySelector } from './directory-selector';

type Sandbox = Prisma.SandboxGetPayload<object>;

interface AppRunnerProps {
sandbox: Sandbox | undefined;
}

export function AppRunner({ sandbox }: AppRunnerProps) {
const [showStartConfirm, setShowStartConfirm] = useState(false);
const [deployDirectory, setDeployDirectory] = useState('./');
const {
isStartingApp,
isStoppingApp,
isAppRunning,
startApp,
stopApp,
} = useAppRunner(sandbox?.id, deployDirectory);

// Toggle app start/stop
const handleToggleApp = () => {
if (isAppRunning) {
stopApp();
} else {
setShowStartConfirm(true); // Open confirmation modal
}
};

const handleConfirmStart = () => {
setShowStartConfirm(false);
startApp();
};

return (
<>
<div className="flex items-center gap-2">
{/* Directory Selector */}
<DirectorySelector
sandboxId={sandbox?.id}
value={deployDirectory}
onChange={setDeployDirectory}
/>



{/* Run App Button */}
<button
onClick={handleToggleApp}
disabled={isStartingApp || isStoppingApp || !sandbox}
className={cn(
'px-2 py-1 text-xs rounded transition-colors flex items-center gap-1 disabled:cursor-not-allowed',
isAppRunning
? 'text-green-400 hover:text-red-400 hover:bg-red-400/10 bg-green-400/10'
: 'text-gray-300 hover:text-white hover:bg-[#37373d] disabled:opacity-50'
)}
title={
isAppRunning
? 'Click to stop. Your app will no longer be accessible.'
: 'Build and run your app in production mode. It will keep running even if you close this terminal.'
}
>
{isStartingApp || isStoppingApp ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : isAppRunning ? (
<Square className="h-3 w-3" />
) : (
<Play className="h-3 w-3" />
)}
<span>
{isStartingApp ? 'Starting...' : isStoppingApp ? 'Stopping...' : isAppRunning ? 'Running' : 'Run App'}
</span>
</button>
</div>

{/* Separator */}
<div className="h-4 w-[1px] bg-[#3e3e42]" />

{/* Confirmation Alert Dialog */}
<AppRunnerDialog
open={showStartConfirm}
onOpenChange={setShowStartConfirm}
onConfirm={handleConfirmStart}
sandboxUrl={sandbox?.publicUrl}
/>
</>
);
}
Loading