diff --git a/app/api/tasks/[taskId]/diff/route.ts b/app/api/tasks/[taskId]/diff/route.ts new file mode 100644 index 00000000..398a8519 --- /dev/null +++ b/app/api/tasks/[taskId]/diff/route.ts @@ -0,0 +1,144 @@ +import { NextRequest, NextResponse } from 'next/server' +import { db } from '@/lib/db/client' +import { tasks } from '@/lib/db/schema' +import { eq } from 'drizzle-orm' +import { octokit } from '@/lib/github/client' + +function getLanguageFromFilename(filename: string): string { + const ext = filename.split('.').pop()?.toLowerCase() + const langMap: { [key: string]: string } = { + js: 'javascript', + jsx: 'javascript', + ts: 'typescript', + tsx: 'typescript', + py: 'python', + java: 'java', + cpp: 'cpp', + c: 'c', + cs: 'csharp', + php: 'php', + rb: 'ruby', + go: 'go', + rs: 'rust', + swift: 'swift', + kt: 'kotlin', + scala: 'scala', + sh: 'bash', + yaml: 'yaml', + yml: 'yaml', + json: 'json', + xml: 'xml', + html: 'html', + css: 'css', + scss: 'scss', + less: 'less', + md: 'markdown', + sql: 'sql', + } + return langMap[ext || ''] || 'text' +} + +async function getFileContent(owner: string, repo: string, path: string, ref: string): Promise { + try { + const response = await octokit.rest.repos.getContent({ + owner, + repo, + path, + ref, + }) + + if ('content' in response.data && typeof response.data.content === 'string') { + return Buffer.from(response.data.content, 'base64').toString('utf-8') + } + + return '' + } catch (error: unknown) { + // File might not exist in this ref (e.g., new file) + if (error && typeof error === 'object' && 'status' in error && error.status === 404) { + return '' + } + throw error + } +} + +export async function GET(request: NextRequest, { params }: { params: Promise<{ taskId: string }> }) { + try { + const { taskId } = await params + const searchParams = request.nextUrl.searchParams + const filename = searchParams.get('filename') + + if (!filename) { + return NextResponse.json({ error: 'Missing filename parameter' }, { status: 400 }) + } + + // Get task from database + const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId)).limit(1) + + if (!task) { + return NextResponse.json({ error: 'Task not found' }, { status: 404 }) + } + + if (!task.branchName || !task.repoUrl) { + return NextResponse.json({ error: 'Task does not have branch or repository information' }, { status: 400 }) + } + + // Parse GitHub repository URL to get owner and repo + const githubMatch = task.repoUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)/) + if (!githubMatch) { + return NextResponse.json({ error: 'Invalid GitHub repository URL' }, { status: 400 }) + } + + const [, owner, repo] = githubMatch + + try { + // Get file content from both main/master and the task branch + let oldContent = '' + let newContent = '' + + // Try to get content from main branch first, fallback to master + try { + oldContent = await getFileContent(owner, repo, filename, 'main') + } catch (error: unknown) { + if (error && typeof error === 'object' && 'status' in error && error.status === 404) { + try { + oldContent = await getFileContent(owner, repo, filename, 'master') + } catch (masterError: unknown) { + if ( + !(masterError && typeof masterError === 'object' && 'status' in masterError && masterError.status === 404) + ) { + throw masterError + } + // File doesn't exist in main/master (could be a new file) + oldContent = '' + } + } else { + throw error + } + } + + // Get content from the task branch + newContent = await getFileContent(owner, repo, filename, task.branchName) + + return NextResponse.json({ + success: true, + data: { + filename, + oldContent, + newContent, + language: getLanguageFromFilename(filename), + }, + }) + } catch (error: unknown) { + console.error('Error fetching file content from GitHub:', error) + return NextResponse.json({ error: 'Failed to fetch file content from GitHub' }, { status: 500 }) + } + } catch (error) { + console.error('Error in diff API:', error) + return NextResponse.json( + { + error: error instanceof Error ? error.message : 'Internal server error', + }, + { status: 500 }, + ) + } +} diff --git a/app/api/tasks/[taskId]/files/route.ts b/app/api/tasks/[taskId]/files/route.ts new file mode 100644 index 00000000..f8b003f1 --- /dev/null +++ b/app/api/tasks/[taskId]/files/route.ts @@ -0,0 +1,225 @@ +import { NextRequest, NextResponse } from 'next/server' +import { db } from '@/lib/db/client' +import { tasks } from '@/lib/db/schema' +import { eq } from 'drizzle-orm' +import { octokit } from '@/lib/github/client' + +interface FileChange { + filename: string + status: 'added' | 'modified' | 'deleted' | 'renamed' + additions: number + deletions: number + changes: number +} + +interface FileTreeNode { + type: 'file' | 'directory' + filename?: string + status?: string + additions?: number + deletions?: number + changes?: number + children?: { [key: string]: FileTreeNode } +} + +export async function GET(request: NextRequest, { params }: { params: Promise<{ taskId: string }> }) { + try { + const { taskId } = await params + + // Get task from database + const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId)).limit(1) + + if (!task) { + return NextResponse.json({ success: false, error: 'Task not found' }, { status: 404 }) + } + + // Check if task has a branch assigned + if (!task.branchName) { + return NextResponse.json({ + success: true, + files: [], + fileTree: {}, + branchName: null, + }) + } + + // Extract owner and repo from the repository URL + const repoUrl = task.repoUrl + if (!repoUrl) { + return NextResponse.json({ + success: true, + files: [], + fileTree: {}, + branchName: task.branchName, + }) + } + + // Parse GitHub repository URL to get owner and repo + const githubMatch = repoUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)/) + if (!githubMatch) { + console.error('Invalid GitHub URL format:', repoUrl) + return NextResponse.json( + { + success: false, + error: 'Invalid repository URL format', + }, + { status: 400 }, + ) + } + + const [, owner, repo] = githubMatch + + let files: FileChange[] = [] + + try { + // First check if the branch exists + try { + await octokit.rest.repos.getBranch({ + owner, + repo, + branch: task.branchName, + }) + } catch (branchError: unknown) { + if (branchError && typeof branchError === 'object' && 'status' in branchError && branchError.status === 404) { + // Branch doesn't exist yet (task is still processing) + console.log(`Branch ${task.branchName} doesn't exist yet, returning empty file list`) + return NextResponse.json({ + success: true, + files: [], + fileTree: {}, + branchName: task.branchName, + message: 'Branch is being created...', + }) + } else { + throw branchError + } + } + + // Try to get the comparison between the branch and main + let comparison + try { + comparison = await octokit.rest.repos.compareCommits({ + owner, + repo, + base: 'main', + head: task.branchName, + }) + } catch (mainError: unknown) { + if (mainError && typeof mainError === 'object' && 'status' in mainError && mainError.status === 404) { + // If main branch doesn't exist, try master + try { + comparison = await octokit.rest.repos.compareCommits({ + owner, + repo, + base: 'master', + head: task.branchName, + }) + } catch (masterError: unknown) { + if ( + masterError && + typeof masterError === 'object' && + 'status' in masterError && + masterError.status === 404 + ) { + // Neither main nor master exists, or head branch doesn't exist + console.log(`Could not compare branches for ${task.branchName}`) + return NextResponse.json({ + success: true, + files: [], + fileTree: {}, + branchName: task.branchName, + message: 'No base branch found for comparison', + }) + } else { + throw masterError + } + } + } else { + throw mainError + } + } + + // Convert GitHub API response to our FileChange format + files = + comparison.data.files?.map((file) => ({ + filename: file.filename, + status: file.status as 'added' | 'modified' | 'deleted' | 'renamed', + additions: file.additions || 0, + deletions: file.deletions || 0, + changes: file.changes || 0, + })) || [] + + console.log(`Found ${files.length} changed files in branch ${task.branchName}`) + } catch (error: unknown) { + console.error('Error fetching file changes from GitHub:', error) + + // If it's a 404 error, return empty results instead of failing + if (error && typeof error === 'object' && 'status' in error && error.status === 404) { + console.log(`Branch or repository not found, returning empty file list`) + return NextResponse.json({ + success: true, + files: [], + fileTree: {}, + branchName: task.branchName, + message: 'Branch not found or still being created', + }) + } + + return NextResponse.json( + { + success: false, + error: 'Failed to fetch file changes from GitHub', + }, + { status: 500 }, + ) + } + + // Build file tree from files + const fileTree: { [key: string]: FileTreeNode } = {} + + for (const file of files) { + addToFileTree(fileTree, file.filename, file) + } + + return NextResponse.json({ + success: true, + files, + fileTree, + branchName: task.branchName, + }) + } catch (error) { + console.error('Error fetching task files:', error) + return NextResponse.json({ success: false, error: 'Failed to fetch task files' }, { status: 500 }) + } +} + +function addToFileTree(tree: { [key: string]: FileTreeNode }, filename: string, fileObj: FileChange) { + const parts = filename.split('/') + let currentLevel = tree + + for (let i = 0; i < parts.length; i++) { + const part = parts[i] + const isLastPart = i === parts.length - 1 + + if (isLastPart) { + // It's a file + currentLevel[part] = { + type: 'file', + filename: fileObj.filename, + status: fileObj.status, + additions: fileObj.additions, + deletions: fileObj.deletions, + changes: fileObj.changes, + } + } else { + // It's a directory + if (!currentLevel[part]) { + currentLevel[part] = { + type: 'directory', + children: {}, + } + } + currentLevel = currentLevel[part].children! + } + } +} diff --git a/components/app-layout.tsx b/components/app-layout.tsx index 51acd2d3..97418e0c 100644 --- a/components/app-layout.tsx +++ b/components/app-layout.tsx @@ -21,6 +21,7 @@ interface TasksContextType { refreshTasks: () => Promise toggleSidebar: () => void isSidebarOpen: boolean + isSidebarResizing: boolean addTaskOptimistically: (taskData: { prompt: string repoUrl: string @@ -265,7 +266,15 @@ export function AppLayout({ children, initialSidebarWidth, initialSidebarOpen }: }, [isResizing]) return ( - +