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
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,12 @@ With the introduction of multi-user authentication, the system now properly isol

You can deploy your own version of the coding agent template to Vercel with one click:

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fcoding-agent-template&env=POSTGRES_URL,VERCEL_TOKEN,VERCEL_TEAM_ID,VERCEL_PROJECT_ID,JWE_SECRET,ENCRYPTION_KEY&envDescription=Required+infrastructure+environment+variables.+You+will+also+need+to+configure+OAuth+(Vercel+or+GitHub)+for+user+authentication.+Optional+API+keys+can+be+added+later.&project-name=coding-agent-template&repository-name=coding-agent-template)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fcoding-agent-template&env=VERCEL_TEAM_ID,VERCEL_PROJECT_ID,VERCEL_TOKEN,JWE_SECRET,ENCRYPTION_KEY&envDescription=Required+environment+variables+for+the+coding+agent+template.+Database+will+be+automatically+provisioned+via+Neon.+You+must+also+configure+at+least+one+OAuth+provider+(GitHub+or+Vercel).+Optional+API+keys+can+be+added+later+in+project+settings.&project-name=coding-agent-template&repository-name=coding-agent-template)

**What happens during deployment:**
- **Automatic Database Setup**: A Neon Postgres database is automatically created and connected to your project
- **Environment Configuration**: You'll be prompted to provide required environment variables (Vercel credentials and encryption keys)
- **OAuth Setup**: After deployment, you'll need to configure at least one OAuth provider (GitHub or Vercel) in your project settings for user authentication

## Features

Expand Down Expand Up @@ -254,13 +259,15 @@ Create a `.env.local` file with your values:

These are set once by you (the app developer) and are used for core infrastructure:

- `POSTGRES_URL`: Your PostgreSQL connection string (works with any PostgreSQL database)
- `POSTGRES_URL`: Your PostgreSQL connection string (automatically provided when deploying to Vercel via the Neon integration, or set manually for local development)
- `VERCEL_TOKEN`: Your Vercel API token (for creating sandboxes)
- `VERCEL_TEAM_ID`: Your Vercel team ID (for sandbox creation)
- `VERCEL_PROJECT_ID`: Your Vercel project ID (for sandbox creation)
- `JWE_SECRET`: Base64-encoded secret for session encryption (generate with: `openssl rand -base64 32`)
- `ENCRYPTION_KEY`: 32-byte hex string for encrypting user API keys and tokens (generate with: `openssl rand -hex 32`)

> **Note**: When deploying to Vercel using the "Deploy with Vercel" button, the database is automatically provisioned via Neon and `POSTGRES_URL` is set for you. For local development, you'll need to provide your own database connection string.

#### User Authentication (Required)

**You must configure at least one authentication method** (Vercel or GitHub):
Expand Down Expand Up @@ -393,7 +400,7 @@ Open [http://localhost:3000](http://localhost:3000) in your browser.
See the [Set up environment variables](#3-set-up-environment-variables) section above for a complete guide.

**Key Points:**
- **Infrastructure**: Set `POSTGRES_URL`, `VERCEL_TOKEN`, `VERCEL_TEAM_ID`, `VERCEL_PROJECT_ID`, `JWE_SECRET`, and `ENCRYPTION_KEY` as the app developer
- **Infrastructure**: Set `VERCEL_TOKEN`, `VERCEL_TEAM_ID`, `VERCEL_PROJECT_ID`, `JWE_SECRET`, and `ENCRYPTION_KEY` as the app developer (database is auto-provisioned on Vercel)
- **Authentication**: Configure at least one OAuth method (Vercel or GitHub) for user sign-in
- **API Keys**: Can be set globally or left for users to provide their own (per-user keys take precedence)
- **GitHub Access**: Users authenticate with their own GitHub accounts - no shared `GITHUB_TOKEN` needed!
Expand Down
41 changes: 41 additions & 0 deletions app/api/github-stars/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { NextResponse } from 'next/server'

const GITHUB_REPO = 'vercel-labs/coding-agent-template'
const CACHE_DURATION = 5 * 60 // 5 minutes in seconds

let cachedStars: number | null = null
let lastFetch = 0

export async function GET() {
try {
const now = Date.now()

// Return cached value if still fresh
if (cachedStars !== null && now - lastFetch < CACHE_DURATION * 1000) {
return NextResponse.json({ stars: cachedStars })
}

// Fetch fresh data
const response = await fetch(`https://api.github.com/repos/${GITHUB_REPO}`, {
headers: {
Accept: 'application/vnd.github+json',
'User-Agent': 'coding-agent-template',
},
next: { revalidate: CACHE_DURATION },
})

if (!response.ok) {
throw new Error('GitHub API request failed')
}

const data = await response.json()
cachedStars = data.stargazers_count
lastFetch = now

return NextResponse.json({ stars: cachedStars })
} catch (error) {
console.error('Error fetching GitHub stars:', error)
// Return cached value or fallback
return NextResponse.json({ stars: cachedStars || 994 })
}
}
40 changes: 34 additions & 6 deletions app/api/tasks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@
}
})

// Get user's API keys and GitHub token BEFORE entering after() block (where session is not accessible)
const userApiKeys = await getUserApiKeys()
const userGithubToken = await getUserGitHubToken()

// Process the task asynchronously with timeout
// CRITICAL: Wrap in after() to ensure Vercel doesn't kill the function after response
// Without this, serverless functions terminate immediately after sending the response
Expand All @@ -159,6 +163,8 @@
validatedData.selectedModel,
validatedData.installDependencies || false,
validatedData.maxDuration || 5,
userApiKeys,
userGithubToken,
)
} catch (error) {
console.error('Task processing failed:', error)
Expand All @@ -181,6 +187,14 @@
selectedModel?: string,
installDependencies: boolean = false,
maxDuration: number = 5,
apiKeys?: {
OPENAI_API_KEY?: string
GEMINI_API_KEY?: string
CURSOR_API_KEY?: string
ANTHROPIC_API_KEY?: string
AI_GATEWAY_API_KEY?: string
},
githubToken?: string | null,
) {
const TASK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes in milliseconds

Expand All @@ -205,7 +219,17 @@

try {
await Promise.race([
processTask(taskId, prompt, repoUrl, selectedAgent, selectedModel, installDependencies, maxDuration),
processTask(
taskId,
prompt,
repoUrl,
selectedAgent,
selectedModel,
installDependencies,
maxDuration,
apiKeys,
githubToken,
),
timeoutPromise,
])

Expand Down Expand Up @@ -272,10 +296,18 @@
selectedModel?: string,
installDependencies: boolean = false,
maxDuration: number = 5,
apiKeys?: {
OPENAI_API_KEY?: string
GEMINI_API_KEY?: string
CURSOR_API_KEY?: string
ANTHROPIC_API_KEY?: string
AI_GATEWAY_API_KEY?: string
},
githubToken?: string | null,
) {
let sandbox: Sandbox | null = null
const logger = createTaskLogger(taskId)
const taskStartTime = Date.now()

Check warning on line 310 in app/api/tasks/route.ts

View workflow job for this annotation

GitHub Actions / checks

'taskStartTime' is assigned a value but never used

try {
console.log('Starting task processing')
Expand All @@ -284,14 +316,10 @@
await logger.updateStatus('processing', 'Task created, preparing to start...')
await logger.updateProgress(10, 'Initializing task execution...')

// Get user's GitHub token for repository access
const githubToken = await getUserGitHubToken()
// GitHub token and API keys are passed as parameters (retrieved before entering after() block)
if (githubToken) {
await logger.info('Using authenticated GitHub access')
}

// Get user's API keys (with fallback to system keys)
const apiKeys = await getUserApiKeys()
await logger.info('API keys configured for selected agent')

// Check if task was stopped before we even start
Expand Down
28 changes: 28 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,31 @@ button {
background-position: -200% 0;
}
}

/* Git Diff Viewer Mobile Optimizations */
.git-diff-view-container {
width: 100%;
overflow-x: auto;
}

/* Make diff viewer more mobile-friendly */
@media (max-width: 768px) {
.git-diff-view-container {
font-size: 11px;
}

/* Ensure horizontal scrolling works on mobile */
.git-diff-view-container > div {
min-width: max-content;
}

/* Adjust line numbers and content for mobile */
.git-diff-view-container .diff-line-num {
padding: 2px 4px;
font-size: 10px;
}

.git-diff-view-container .diff-line-content {
padding: 2px 4px;
}
}
4 changes: 3 additions & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { cookies } from 'next/headers'
import { HomePageContent } from '@/components/home-page-content'
import { getServerSession } from '@/lib/session/get-server-session'
import { getGitHubStars } from '@/lib/github-stars'

export default async function Home() {
const cookieStore = await cookies()
Expand All @@ -9,7 +10,7 @@ export default async function Home() {
const installDependencies = cookieStore.get('install-dependencies')?.value === 'true'
const maxDuration = parseInt(cookieStore.get('max-duration')?.value || '5', 10)

const session = await getServerSession()
const [session, stars] = await Promise.all([getServerSession(), getGitHubStars()])

return (
<HomePageContent
Expand All @@ -18,6 +19,7 @@ export default async function Home() {
initialInstallDependencies={installDependencies}
initialMaxDuration={maxDuration}
user={session?.user ?? null}
initialStars={stars}
/>
)
}
82 changes: 16 additions & 66 deletions app/tasks/[taskId]/loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,95 +3,45 @@
import { PageHeader } from '@/components/page-header'
import { useTasks } from '@/components/app-layout'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { MoreHorizontal } from 'lucide-react'
import { Loader2 } from 'lucide-react'
import { VERCEL_DEPLOY_URL } from '@/lib/constants'
import { GitHubStarsButton } from '@/components/github-stars-button'

export default function TaskLoading() {
const { toggleSidebar } = useTasks()

// Placeholder actions for loading state
// Placeholder actions for loading state - no user avatar to prevent flash
const loadingActions = (
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 h-8">
<GitHubStarsButton />
{/* Deploy to Vercel Button */}
<Button
asChild
variant="outline"
size="sm"
className="h-8 px-3 text-xs bg-black text-white border-black hover:bg-black/90 dark:bg-white dark:text-black dark:border-white dark:hover:bg-white/90"
className="h-8 sm:px-3 px-0 sm:w-auto w-8 bg-black text-white border-black hover:bg-black/90 dark:bg-white dark:text-black dark:border-white dark:hover:bg-white/90"
>
<a href={VERCEL_DEPLOY_URL} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1.5">
<svg viewBox="0 0 76 65" className="h-3 w-3" fill="currentColor">
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" />
</svg>
Deploy to Vercel
<span className="hidden sm:inline">Deploy Your Own</span>
</a>
</Button>

{/* More Actions Menu Placeholder */}
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" disabled>
<MoreHorizontal className="h-4 w-4" />
</Button>
{/* Empty spacer to reserve space for user avatar */}
<div className="w-8" />
</div>
)

return (
<div className="flex-1 bg-background">
<div className="mx-auto p-3">
<div className="flex-1 bg-background flex flex-col">
<div className="p-3">
<PageHeader showMobileMenu={true} onToggleMobileMenu={toggleSidebar} actions={loadingActions} />

<div className="max-w-4xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Task Info Skeleton */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="h-6 bg-muted animate-pulse rounded w-24"></div>
<div className="h-6 bg-muted animate-pulse rounded w-16"></div>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="h-4 bg-muted animate-pulse rounded w-16"></div>
<div className="h-16 bg-muted animate-pulse rounded"></div>
</div>
<div className="space-y-2">
<div className="h-4 bg-muted animate-pulse rounded w-20"></div>
<div className="h-4 bg-muted animate-pulse rounded w-48"></div>
</div>
<div className="space-y-2">
<div className="h-4 bg-muted animate-pulse rounded w-12"></div>
<div className="flex items-center gap-2">
<div className="h-4 w-4 bg-muted animate-pulse rounded"></div>
<div className="h-4 bg-muted animate-pulse rounded w-24"></div>
</div>
</div>
</CardContent>
</Card>

{/* Logs Skeleton */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="h-6 bg-muted animate-pulse rounded w-20"></div>
<div className="h-8 bg-muted animate-pulse rounded w-16"></div>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3 h-96 overflow-hidden">
{Array.from({ length: 8 }).map((_, i) => {
const widths = ['75%', '85%', '65%', '90%', '70%', '80%', '95%', '60%']
return (
<div key={i} className="flex gap-2">
<div className="h-4 w-12 bg-muted animate-pulse rounded flex-shrink-0"></div>
<div className="h-4 bg-muted animate-pulse rounded flex-1" style={{ width: widths[i] }}></div>
</div>
)
})}
</div>
</CardContent>
</Card>
</div>
</div>
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-3 text-muted-foreground" />
<p className="text-sm text-muted-foreground">Loading task...</p>
</div>
</div>
</div>
Expand Down
12 changes: 11 additions & 1 deletion app/tasks/[taskId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { TaskPageClient } from '@/components/task-page-client'
import { getServerSession } from '@/lib/session/get-server-session'
import { getGitHubStars } from '@/lib/github-stars'

interface TaskPageProps {
params: {
Expand All @@ -8,8 +10,16 @@ interface TaskPageProps {

export default async function TaskPage({ params }: TaskPageProps) {
const { taskId } = await params
const [session, stars] = await Promise.all([getServerSession(), getGitHubStars()])

return <TaskPageClient taskId={taskId} />
return (
<TaskPageClient
taskId={taskId}
user={session?.user ?? null}
authProvider={session?.authProvider ?? null}
initialStars={stars}
/>
)
}

export async function generateMetadata({ params }: TaskPageProps) {
Expand Down
13 changes: 11 additions & 2 deletions components/app-layout-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cookies } from 'next/headers'
import { cookies, headers } from 'next/headers'
import { AppLayout } from './app-layout'
import { getSidebarWidthFromCookie, getSidebarOpenFromCookie } from '@/lib/utils/cookies'

Expand All @@ -12,8 +12,17 @@ export async function AppLayoutWrapper({ children }: AppLayoutWrapperProps) {
const initialSidebarWidth = getSidebarWidthFromCookie(cookieString)
const initialSidebarOpen = getSidebarOpenFromCookie(cookieString)

// Detect if mobile from user agent
const headersList = await headers()
const userAgent = headersList.get('user-agent') || ''
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)

return (
<AppLayout initialSidebarWidth={initialSidebarWidth} initialSidebarOpen={initialSidebarOpen}>
<AppLayout
initialSidebarWidth={initialSidebarWidth}
initialSidebarOpen={initialSidebarOpen}
initialIsMobile={isMobile}
>
{children}
</AppLayout>
)
Expand Down
Loading
Loading