From 12531cf918425fd2fffecdf948f611c59fcbe347 Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:00:47 +0800 Subject: [PATCH 1/7] refactor: move actions to root and add sandbox runCommand - Move lib/actions to root /actions directory - Implement runCommand action in actions/sandbox.ts - Extract TTYD context helper to lib/util/ttyd-context.ts - Create actions/types.ts for shared action types - Update component imports --- actions/sandbox.ts | 65 ++++++++++++++++++ {lib/actions => actions}/sealos-auth.ts | 0 actions/types.ts | 13 ++++ components/home-page.tsx | 2 +- lib/util/ttyd-context.ts | 87 +++++++++++++++++++++++++ 5 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 actions/sandbox.ts rename {lib/actions => actions}/sealos-auth.ts (100%) create mode 100644 actions/types.ts create mode 100644 lib/util/ttyd-context.ts diff --git a/actions/sandbox.ts b/actions/sandbox.ts new file mode 100644 index 0000000..49653fe --- /dev/null +++ b/actions/sandbox.ts @@ -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 { + 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 { + // TODO: Implement detached command execution + throw new Error('Not implemented') +} diff --git a/lib/actions/sealos-auth.ts b/actions/sealos-auth.ts similarity index 100% rename from lib/actions/sealos-auth.ts rename to actions/sealos-auth.ts diff --git a/actions/types.ts b/actions/types.ts new file mode 100644 index 0000000..b1f8012 --- /dev/null +++ b/actions/types.ts @@ -0,0 +1,13 @@ +/** + * Type definitions for Server Actions + */ + +// ============================================================================= +// Sandbox Actions +// ============================================================================= + +export type ExecResult = { + success: boolean + output?: string + error?: string +} diff --git a/components/home-page.tsx b/components/home-page.tsx index b23f608..7bf2cc8 100644 --- a/components/home-page.tsx +++ b/components/home-page.tsx @@ -8,7 +8,7 @@ import { useSession } from 'next-auth/react'; import { MatrixRain } from '@/components/MatrixRain'; import { Button } from '@/components/ui/button'; -import { authenticateWithSealos } from '@/lib/actions/sealos-auth'; +import { authenticateWithSealos } from '@/actions/sealos-auth'; import { useSealos } from '@/provider/sealos'; /** diff --git a/lib/util/ttyd-context.ts b/lib/util/ttyd-context.ts new file mode 100644 index 0000000..5b32d80 --- /dev/null +++ b/lib/util/ttyd-context.ts @@ -0,0 +1,87 @@ +/** + * TTYD Context Utilities + * + * Helper functions to get TTYD connection context for sandboxes. + */ + +import { prisma } from '@/lib/db' + +// ============================================================================= +// Types +// ============================================================================= + +export interface TtydContext { + baseUrl: string + accessToken: string + authorization?: string +} + +export interface SandboxTtydContext { + ttyd: TtydContext + sandbox: Awaited> +} + +// ============================================================================= +// Internal Helpers +// ============================================================================= + +async function getSandboxWithTtyd(sandboxId: string, userId: string) { + const sandbox = await prisma.sandbox.findFirst({ + where: { + id: sandboxId, + project: { + userId: userId, + }, + }, + include: { + project: { + include: { + environments: true, + }, + }, + }, + }) + + if (!sandbox) { + throw new Error('Sandbox not found') + } + + return sandbox +} + +// ============================================================================= +// Public Functions +// ============================================================================= + +/** + * Get TTYD connection context for a sandbox. + * Verifies the sandbox belongs to the specified user. + * + * @param sandboxId - The sandbox ID + * @param userId - The user ID (for ownership verification) + * @returns TTYD connection context with baseUrl, accessToken, and sandbox + */ +export async function getSandboxTtydContext( + sandboxId: string, + userId: string +): Promise { + const sandbox = await getSandboxWithTtyd(sandboxId, userId) + + const accessToken = sandbox.project.environments.find( + (env) => env.key === 'TTYD_ACCESS_TOKEN' + )?.value + + if (!sandbox.ttydUrl || !accessToken) { + throw new Error('Sandbox TTYD not configured') + } + + // Parse the ttydUrl to get base URL (without query params) + const ttydBaseUrl = new URL(sandbox.ttydUrl) + const authorization = ttydBaseUrl.searchParams.get('authorization') || undefined + ttydBaseUrl.search = '' + const baseUrl = ttydBaseUrl.toString().replace(/\/$/, '') + + const ttyd = { baseUrl, accessToken, authorization } + + return { ttyd, sandbox } +} From 723fd8ec2879a2ca75eae864dbded5e4b36cf38c Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Mon, 19 Jan 2026 20:49:19 +0800 Subject: [PATCH 2/7] refactor: restructure terminal toolbar component - Move terminal-toolbar.tsx to toolbar/toolbar.tsx - Update imports in terminal-container.tsx --- components/terminal/terminal-container.tsx | 2 +- .../toolbar.tsx} | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) rename components/terminal/{terminal-toolbar.tsx => toolbar/toolbar.tsx} (94%) diff --git a/components/terminal/terminal-container.tsx b/components/terminal/terminal-container.tsx index 17bb233..343163a 100644 --- a/components/terminal/terminal-container.tsx +++ b/components/terminal/terminal-container.tsx @@ -17,7 +17,7 @@ import { useState } from 'react'; import type { Prisma } from '@prisma/client'; import { TerminalDisplay } from './terminal-display'; -import { type Tab, TerminalToolbar } from './terminal-toolbar'; +import { type Tab, TerminalToolbar } from './toolbar/toolbar'; // ============================================================================ // Types diff --git a/components/terminal/terminal-toolbar.tsx b/components/terminal/toolbar/toolbar.tsx similarity index 94% rename from components/terminal/terminal-toolbar.tsx rename to components/terminal/toolbar/toolbar.tsx index 52ae9ed..aa15e3f 100644 --- a/components/terminal/terminal-toolbar.tsx +++ b/components/terminal/toolbar/toolbar.tsx @@ -8,7 +8,7 @@ import { useEffect, useState } from 'react'; import type { Prisma } from '@prisma/client'; -import { Copy, Eye, EyeOff, Loader2, Network, Play, Plus, Square, Terminal as TerminalIcon, X } from 'lucide-react'; +import { Copy, Eye, EyeOff, Folder, Loader2, Network, Play, Plus, Square, Terminal as TerminalIcon, X } from 'lucide-react'; import { toast } from 'sonner'; import { @@ -287,6 +287,25 @@ export function TerminalToolbar({ {project.status} */} + {/* Directory Selector */} +
+
+
+ +
+ +
+ + {/* Separator */} +
+
+ {/* Run App Button (was Deploy) */}