diff --git a/app/api/tasks/[taskId]/deployment/route.ts b/app/api/tasks/[taskId]/deployment/route.ts
new file mode 100644
index 0000000..6d8ecd8
--- /dev/null
+++ b/app/api/tasks/[taskId]/deployment/route.ts
@@ -0,0 +1,315 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { getServerSession } from '@/lib/session/get-server-session'
+import { db } from '@/lib/db/client'
+import { tasks } from '@/lib/db/schema'
+import { eq, and, isNull } from 'drizzle-orm'
+import { getOctokit } from '@/lib/github/client'
+
+export async function GET(request: NextRequest, { params }: { params: Promise<{ taskId: string }> }) {
+ try {
+ const session = await getServerSession()
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const { taskId } = await params
+
+ // Get task from database
+ const taskResult = await db
+ .select()
+ .from(tasks)
+ .where(and(eq(tasks.id, taskId), eq(tasks.userId, session.user.id), isNull(tasks.deletedAt)))
+ .limit(1)
+
+ const task = taskResult[0]
+
+ if (!task) {
+ return NextResponse.json({ error: 'Task not found' }, { status: 404 })
+ }
+
+ // Return cached preview URL if available
+ if (task.previewUrl) {
+ return NextResponse.json({
+ success: true,
+ data: {
+ hasDeployment: true,
+ previewUrl: task.previewUrl,
+ cached: true,
+ },
+ })
+ }
+
+ // Return early if no branch or repo
+ if (!task.branchName || !task.repoUrl) {
+ return NextResponse.json({
+ success: true,
+ data: {
+ hasDeployment: false,
+ message: 'Task does not have branch or repository information',
+ },
+ })
+ }
+
+ // Parse GitHub repository URL to get owner and repo
+ const githubMatch = task.repoUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)/)
+ if (!githubMatch) {
+ return NextResponse.json({
+ success: true,
+ data: {
+ hasDeployment: false,
+ message: 'Invalid GitHub repository URL',
+ },
+ })
+ }
+
+ const [, owner, repo] = githubMatch
+
+ try {
+ const octokit = await getOctokit()
+
+ // Check if user has GitHub connected
+ if (!octokit.auth) {
+ return NextResponse.json({
+ success: true,
+ data: {
+ hasDeployment: false,
+ message: 'GitHub account not connected',
+ },
+ })
+ }
+
+ // First, get the latest commit on the branch to check for deployment checks
+ let latestCommitSha: string | null = null
+ try {
+ const { data: branch } = await octokit.rest.repos.getBranch({
+ owner,
+ repo,
+ branch: task.branchName,
+ })
+ latestCommitSha = branch.commit.sha
+ } catch (branchError) {
+ if (branchError && typeof branchError === 'object' && 'status' in branchError && branchError.status === 404) {
+ return NextResponse.json({
+ success: true,
+ data: {
+ hasDeployment: false,
+ message: 'Branch not found',
+ },
+ })
+ }
+ throw branchError
+ }
+
+ // Check for Vercel deployment via GitHub Checks API (most common)
+ if (latestCommitSha) {
+ try {
+ const { data: checkRuns } = await octokit.rest.checks.listForRef({
+ owner,
+ repo,
+ ref: latestCommitSha,
+ per_page: 100,
+ })
+
+ // Helper function to extract preview URL from check output
+ const extractPreviewUrl = (check: {
+ output?: { summary?: string | null; text?: string | null } | null
+ }): string | null => {
+ // Check output summary for deployment URL
+ if (check.output?.summary) {
+ const summary = check.output.summary
+ // Look for URLs in the format https://[deployment]-[hash].vercel.app or other Vercel domains
+ const urlMatch = summary.match(/https?:\/\/[^\s\)\]<]+\.vercel\.app/i)
+ if (urlMatch) {
+ return urlMatch[0]
+ }
+ }
+
+ // Check output text as well
+ if (check.output?.text) {
+ const text = check.output.text
+ const urlMatch = text.match(/https?:\/\/[^\s\)\]<]+\.vercel\.app/i)
+ if (urlMatch) {
+ return urlMatch[0]
+ }
+ }
+
+ return null
+ }
+
+ // Look for Vercel check runs - try Preview Comments first as it's more likely to have the URL
+ const vercelPreviewCheck = checkRuns.check_runs.find(
+ (check) =>
+ check.app?.slug === 'vercel' && check.name === 'Vercel Preview Comments' && check.status === 'completed',
+ )
+
+ const vercelDeploymentCheck = checkRuns.check_runs.find(
+ (check) =>
+ check.app?.slug === 'vercel' &&
+ check.name === 'Vercel' &&
+ check.conclusion === 'success' &&
+ check.status === 'completed',
+ )
+
+ // Try to get preview URL from either check
+ let previewUrl: string | null = null
+
+ if (vercelPreviewCheck) {
+ previewUrl = extractPreviewUrl(vercelPreviewCheck)
+ }
+
+ if (!previewUrl && vercelDeploymentCheck) {
+ previewUrl = extractPreviewUrl(vercelDeploymentCheck)
+ }
+
+ // Fallback to details_url if no preview URL found
+ if (!previewUrl && vercelDeploymentCheck?.details_url) {
+ previewUrl = vercelDeploymentCheck.details_url
+ }
+
+ if (previewUrl) {
+ // Store the preview URL in the database
+ await db.update(tasks).set({ previewUrl }).where(eq(tasks.id, taskId))
+
+ return NextResponse.json({
+ success: true,
+ data: {
+ hasDeployment: true,
+ previewUrl,
+ checkId: vercelDeploymentCheck?.id || vercelPreviewCheck?.id,
+ createdAt: vercelDeploymentCheck?.completed_at || vercelPreviewCheck?.completed_at,
+ },
+ })
+ }
+ } catch (checksError) {
+ console.error('Error checking GitHub Checks:', checksError)
+ // Continue to try other methods
+ }
+ }
+
+ // Fallback: Check GitHub Deployments API
+ try {
+ const { data: deployments } = await octokit.rest.repos.listDeployments({
+ owner,
+ repo,
+ ref: task.branchName,
+ per_page: 10,
+ })
+
+ if (deployments && deployments.length > 0) {
+ // Find the most recent Vercel deployment
+ for (const deployment of deployments) {
+ // Check if this is a Vercel deployment
+ if (
+ deployment.environment === 'Preview' ||
+ deployment.environment === 'preview' ||
+ deployment.description?.toLowerCase().includes('vercel')
+ ) {
+ // Get deployment status
+ const { data: statuses } = await octokit.rest.repos.listDeploymentStatuses({
+ owner,
+ repo,
+ deployment_id: deployment.id,
+ per_page: 1,
+ })
+
+ if (statuses && statuses.length > 0) {
+ const status = statuses[0]
+ if (status.state === 'success') {
+ const previewUrl = status.environment_url || status.target_url
+ if (previewUrl) {
+ // Store the preview URL in the database
+ await db.update(tasks).set({ previewUrl }).where(eq(tasks.id, taskId))
+
+ return NextResponse.json({
+ success: true,
+ data: {
+ hasDeployment: true,
+ previewUrl,
+ deploymentId: deployment.id,
+ createdAt: deployment.created_at,
+ },
+ })
+ }
+ }
+ }
+ }
+ }
+ }
+ } catch (deploymentsError) {
+ console.error('Error checking GitHub Deployments:', deploymentsError)
+ // Continue to final fallback
+ }
+
+ // Final fallback: Check commit statuses
+ if (latestCommitSha) {
+ try {
+ const { data: statuses } = await octokit.rest.repos.listCommitStatusesForRef({
+ owner,
+ repo,
+ ref: latestCommitSha,
+ per_page: 100,
+ })
+
+ const vercelStatus = statuses.find(
+ (status) =>
+ status.context?.toLowerCase().includes('vercel') && status.state === 'success' && status.target_url,
+ )
+
+ if (vercelStatus && vercelStatus.target_url) {
+ // Store the preview URL in the database
+ await db.update(tasks).set({ previewUrl: vercelStatus.target_url }).where(eq(tasks.id, taskId))
+
+ return NextResponse.json({
+ success: true,
+ data: {
+ hasDeployment: true,
+ previewUrl: vercelStatus.target_url,
+ createdAt: vercelStatus.created_at,
+ },
+ })
+ }
+ } catch (statusError) {
+ console.error('Error checking commit statuses:', statusError)
+ }
+ }
+
+ // No deployment found
+ return NextResponse.json({
+ success: true,
+ data: {
+ hasDeployment: false,
+ message: 'No successful Vercel deployment found',
+ },
+ })
+ } catch (error) {
+ console.error('Error fetching deployment status:', error)
+
+ // Return graceful response for common errors
+ if (error && typeof error === 'object' && 'status' in error && error.status === 404) {
+ return NextResponse.json({
+ success: true,
+ data: {
+ hasDeployment: false,
+ message: 'Branch or repository not found',
+ },
+ })
+ }
+
+ return NextResponse.json({
+ success: true,
+ data: {
+ hasDeployment: false,
+ message: 'Failed to fetch deployment status',
+ },
+ })
+ }
+ } catch (error) {
+ console.error('Error in deployment API:', error)
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Internal server error',
+ },
+ { status: 500 },
+ )
+ }
+}
diff --git a/components/app-layout.tsx b/components/app-layout.tsx
index 117feff..e09a167 100644
--- a/components/app-layout.tsx
+++ b/components/app-layout.tsx
@@ -227,6 +227,7 @@ export function AppLayout({ children, initialSidebarWidth, initialSidebarOpen, i
error: null,
branchName: null,
sandboxUrl: null,
+ previewUrl: null,
mcpServerIds: null,
createdAt: new Date(),
updatedAt: new Date(),
diff --git a/components/icons/vercel-icon.tsx b/components/icons/vercel-icon.tsx
new file mode 100644
index 0000000..527cf50
--- /dev/null
+++ b/components/icons/vercel-icon.tsx
@@ -0,0 +1,7 @@
+export default function VercelIcon({ className = 'h-6 w-6' }: { className?: string }) {
+ return (
+
+ )
+}
diff --git a/components/task-details.tsx b/components/task-details.tsx
index b3c259e..0aa4e28 100644
--- a/components/task-details.tsx
+++ b/components/task-details.tsx
@@ -44,6 +44,7 @@ import LinearIcon from '@/components/icons/linear-icon'
import NotionIcon from '@/components/icons/notion-icon'
import PlaywrightIcon from '@/components/icons/playwright-icon'
import SupabaseIcon from '@/components/icons/supabase-icon'
+import VercelIcon from '@/components/icons/vercel-icon'
interface TaskDetailsProps {
task: Task
@@ -124,6 +125,8 @@ export function TaskDetails({ task }: TaskDetailsProps) {
const [isTryingAgain, setIsTryingAgain] = useState(false)
const [selectedAgent, setSelectedAgent] = useState(task.selectedAgent || 'claude')
const [selectedModel, setSelectedModel] = useState(task.selectedModel || DEFAULT_MODELS.claude)
+ const [deploymentUrl, setDeploymentUrl] = useState(task.previewUrl || null)
+ const [loadingDeployment, setLoadingDeployment] = useState(false)
const { refreshTasks } = useTasks()
const router = useRouter()
@@ -285,6 +288,41 @@ export function TaskDetails({ task }: TaskDetailsProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(task.mcpServerIds)])
+ // Fetch deployment info when task is completed and has a branch (only if not already cached)
+ useEffect(() => {
+ async function fetchDeployment() {
+ // Skip if we already have a preview URL or task isn't ready
+ if (deploymentUrl || currentStatus !== 'completed' || !task.branchName) {
+ return
+ }
+
+ setLoadingDeployment(true)
+
+ try {
+ const response = await fetch(`/api/tasks/${task.id}/deployment`)
+ if (response.ok) {
+ const result = await response.json()
+ if (result.success && result.data.hasDeployment && result.data.previewUrl) {
+ setDeploymentUrl(result.data.previewUrl)
+ }
+ }
+ } catch (error) {
+ console.error('Failed to fetch deployment info:', error)
+ } finally {
+ setLoadingDeployment(false)
+ }
+ }
+
+ fetchDeployment()
+ }, [task.id, task.branchName, currentStatus, deploymentUrl])
+
+ // Update deploymentUrl when task.previewUrl changes
+ useEffect(() => {
+ if (task.previewUrl && task.previewUrl !== deploymentUrl) {
+ setDeploymentUrl(task.previewUrl)
+ }
+ }, [task.previewUrl, deploymentUrl])
+
// Fetch all diffs when files list changes
const fetchAllDiffs = async (filesList: string[]) => {
if (!filesList.length || loadingDiffsRef.current) return
@@ -631,6 +669,21 @@ export function TaskDetails({ task }: TaskDetailsProps) {
)}
+
+ {/* Preview Deployment */}
+ {!loadingDeployment && deploymentUrl && (
+
+ )}
diff --git a/lib/db/migrations/0012_burly_captain_britain.sql b/lib/db/migrations/0012_burly_captain_britain.sql
new file mode 100644
index 0000000..e8b87f9
--- /dev/null
+++ b/lib/db/migrations/0012_burly_captain_britain.sql
@@ -0,0 +1 @@
+ALTER TABLE "tasks" ADD COLUMN "preview_url" text;
\ No newline at end of file
diff --git a/lib/db/migrations/meta/0012_snapshot.json b/lib/db/migrations/meta/0012_snapshot.json
new file mode 100644
index 0000000..6d10d81
--- /dev/null
+++ b/lib/db/migrations/meta/0012_snapshot.json
@@ -0,0 +1,603 @@
+{
+ "id": "b133eb97-a7a9-4ac9-a467-21632a8cf5a1",
+ "prevId": "9df31f87-de44-4c99-b222-900db0ab4d0a",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.accounts": {
+ "name": "accounts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'github'"
+ },
+ "external_user_id": {
+ "name": "external_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "accounts_user_id_provider_idx": {
+ "name": "accounts_user_id_provider_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "accounts_user_id_users_id_fk": {
+ "name": "accounts_user_id_users_id_fk",
+ "tableFrom": "accounts",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.connectors": {
+ "name": "connectors",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'remote'"
+ },
+ "base_url": {
+ "name": "base_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_client_id": {
+ "name": "oauth_client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_client_secret": {
+ "name": "oauth_client_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "command": {
+ "name": "command",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "env": {
+ "name": "env",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'disconnected'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "connectors_user_id_users_id_fk": {
+ "name": "connectors_user_id_users_id_fk",
+ "tableFrom": "connectors",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.keys": {
+ "name": "keys",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "keys_user_id_provider_idx": {
+ "name": "keys_user_id_provider_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "keys_user_id_users_id_fk": {
+ "name": "keys_user_id_users_id_fk",
+ "tableFrom": "keys",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tasks": {
+ "name": "tasks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "prompt": {
+ "name": "prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repo_url": {
+ "name": "repo_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "selected_agent": {
+ "name": "selected_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'claude'"
+ },
+ "selected_model": {
+ "name": "selected_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "install_dependencies": {
+ "name": "install_dependencies",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "max_duration": {
+ "name": "max_duration",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 5
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "progress": {
+ "name": "progress",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "logs": {
+ "name": "logs",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "branch_name": {
+ "name": "branch_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sandbox_url": {
+ "name": "sandbox_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "preview_url": {
+ "name": "preview_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mcp_server_ids": {
+ "name": "mcp_server_ids",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tasks_user_id_users_id_fk": {
+ "name": "tasks_user_id_users_id_fk",
+ "tableFrom": "tasks",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "avatar_url": {
+ "name": "avatar_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_login_at": {
+ "name": "last_login_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "users_provider_external_id_idx": {
+ "name": "users_provider_external_id_idx",
+ "columns": [
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/lib/db/migrations/meta/_journal.json b/lib/db/migrations/meta/_journal.json
index 51decca..c18c687 100644
--- a/lib/db/migrations/meta/_journal.json
+++ b/lib/db/migrations/meta/_journal.json
@@ -85,6 +85,13 @@
"when": 1760171942092,
"tag": "0011_outstanding_punisher",
"breakpoints": true
+ },
+ {
+ "idx": 12,
+ "version": "7",
+ "when": 1760236167307,
+ "tag": "0012_burly_captain_britain",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/lib/db/schema.ts b/lib/db/schema.ts
index 92e3175..a3c4016 100644
--- a/lib/db/schema.ts
+++ b/lib/db/schema.ts
@@ -94,6 +94,7 @@ export const tasks = pgTable('tasks', {
error: text('error'),
branchName: text('branch_name'),
sandboxUrl: text('sandbox_url'),
+ previewUrl: text('preview_url'),
mcpServerIds: jsonb('mcp_server_ids').$type(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
@@ -117,6 +118,7 @@ export const insertTaskSchema = z.object({
error: z.string().optional(),
branchName: z.string().optional(),
sandboxUrl: z.string().optional(),
+ previewUrl: z.string().optional(),
mcpServerIds: z.array(z.string()).optional(),
createdAt: z.date().optional(),
updatedAt: z.date().optional(),
@@ -139,6 +141,7 @@ export const selectTaskSchema = z.object({
error: z.string().nullable(),
branchName: z.string().nullable(),
sandboxUrl: z.string().nullable(),
+ previewUrl: z.string().nullable(),
mcpServerIds: z.array(z.string()).nullable(),
createdAt: z.date(),
updatedAt: z.date(),