From ef16acf661fe3587f75d16270c831306393d90d1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 15 Nov 2025 01:49:03 +0000 Subject: [PATCH 1/2] Add chat route /chat/[owner]/[repo] equivalent --- app/chat/[owner]/[repo]/page.tsx | 40 ++++ components/chat-page-content.tsx | 353 +++++++++++++++++++++++++++++++ 2 files changed, 393 insertions(+) create mode 100644 app/chat/[owner]/[repo]/page.tsx create mode 100644 components/chat-page-content.tsx diff --git a/app/chat/[owner]/[repo]/page.tsx b/app/chat/[owner]/[repo]/page.tsx new file mode 100644 index 00000000..55d71a0b --- /dev/null +++ b/app/chat/[owner]/[repo]/page.tsx @@ -0,0 +1,40 @@ +import { cookies } from 'next/headers' +import { ChatPageContent } from '@/components/chat-page-content' +import { getServerSession } from '@/lib/session/get-server-session' +import { getGitHubStars } from '@/lib/github-stars' +import { getMaxSandboxDuration } from '@/lib/db/settings' + +interface ChatPageProps { + params: Promise<{ + owner: string + repo: string + }> +} + +export default async function ChatPage({ params }: ChatPageProps) { + const { owner, repo } = await params + const cookieStore = await cookies() + const installDependencies = cookieStore.get('install-dependencies')?.value === 'true' + const keepAlive = cookieStore.get('keep-alive')?.value === 'true' + + const session = await getServerSession() + + // Get max sandbox duration for this user (user-specific > global > env var) + const maxSandboxDuration = await getMaxSandboxDuration(session?.user?.id) + const maxDuration = parseInt(cookieStore.get('max-duration')?.value || maxSandboxDuration.toString(), 10) + + const stars = await getGitHubStars() + + return ( + + ) +} diff --git a/components/chat-page-content.tsx b/components/chat-page-content.tsx new file mode 100644 index 00000000..7723a349 --- /dev/null +++ b/components/chat-page-content.tsx @@ -0,0 +1,353 @@ +'use client' + +import { useState, useEffect } from 'react' +import { TaskForm } from '@/components/task-form' +import { HomePageHeader } from '@/components/home-page-header' +import { toast } from 'sonner' +import { useRouter } from 'next/navigation' +import { useTasks } from '@/components/app-layout' +import { setSelectedOwner, setSelectedRepo } from '@/lib/utils/cookies' +import type { Session } from '@/lib/session/types' +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { redirectToSignIn } from '@/lib/session/redirect-to-sign-in' +import { GitHubIcon } from '@/components/icons/github-icon' +import { getEnabledAuthProviders } from '@/lib/auth/providers' +import { useSetAtom } from 'jotai' +import { taskPromptAtom } from '@/lib/atoms/task' +import { HomePageMobileFooter } from '@/components/home-page-mobile-footer' + +interface ChatPageContentProps { + owner: string + repo: string + initialInstallDependencies?: boolean + initialMaxDuration?: number + initialKeepAlive?: boolean + maxSandboxDuration?: number + user?: Session['user'] | null + initialStars?: number +} + +export function ChatPageContent({ + owner, + repo, + initialInstallDependencies = false, + initialMaxDuration = 300, + initialKeepAlive = false, + maxSandboxDuration = 300, + user = null, + initialStars = 1200, +}: ChatPageContentProps) { + const [isSubmitting, setIsSubmitting] = useState(false) + const [selectedOwner, setSelectedOwnerState] = useState(owner) + const [selectedRepo, setSelectedRepoState] = useState(repo) + const [showSignInDialog, setShowSignInDialog] = useState(false) + const [loadingVercel, setLoadingVercel] = useState(false) + const [loadingGitHub, setLoadingGitHub] = useState(false) + const router = useRouter() + const { refreshTasks, addTaskOptimistically } = useTasks() + const setTaskPrompt = useSetAtom(taskPromptAtom) + + // Check which auth providers are enabled + const { github: hasGitHub, vercel: hasVercel } = getEnabledAuthProviders() + + // Set cookies for the selected owner/repo on mount + useEffect(() => { + setSelectedOwner(owner) + setSelectedRepo(repo) + }, [owner, repo]) + + // Wrapper functions to update both state and cookies + const handleOwnerChange = (newOwner: string) => { + setSelectedOwnerState(newOwner) + setSelectedOwner(newOwner) + // Navigate to new URL if owner changes + if (selectedRepo) { + router.push(`/chat/${newOwner}/${selectedRepo}`) + } + } + + const handleRepoChange = (newRepo: string) => { + setSelectedRepoState(newRepo) + setSelectedRepo(newRepo) + // Navigate to new URL if repo changes + router.push(`/chat/${selectedOwner}/${newRepo}`) + } + + const handleTaskSubmit = async (data: { + prompt: string + repoUrl: string + selectedAgent: string + selectedModel: string + selectedModels?: string[] + installDependencies: boolean + maxDuration: number + keepAlive: boolean + }) => { + // Check if user is authenticated + if (!user) { + setShowSignInDialog(true) + return + } + + // Check if user has selected a repository + if (!data.repoUrl) { + toast.error('Please select a repository', { + description: 'Choose a GitHub repository to work with from the header.', + }) + return + } + + // Clear the saved prompt since we're actually submitting it now + setTaskPrompt('') + + setIsSubmitting(true) + + // Check if this is multi-agent mode with multiple models selected + const isMultiAgent = data.selectedAgent === 'multi-agent' && data.selectedModels && data.selectedModels.length > 0 + + if (isMultiAgent) { + // Create multiple tasks, one for each selected model + const taskIds: string[] = [] + const tasksData = data.selectedModels!.map((modelValue) => { + // Parse agent:model format + const [agent, model] = modelValue.split(':') + const { id } = addTaskOptimistically({ + prompt: data.prompt, + repoUrl: data.repoUrl, + selectedAgent: agent, + selectedModel: model, + installDependencies: data.installDependencies, + maxDuration: data.maxDuration, + }) + taskIds.push(id) + return { + id, + prompt: data.prompt, + repoUrl: data.repoUrl, + selectedAgent: agent, + selectedModel: model, + installDependencies: data.installDependencies, + maxDuration: data.maxDuration, + keepAlive: data.keepAlive, + } + }) + + // Navigate to the first task + router.push(`/tasks/${taskIds[0]}`) + + try { + // Create all tasks in parallel + const responses = await Promise.all( + tasksData.map((taskData) => + fetch('/api/tasks', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(taskData), + }), + ), + ) + + const successCount = responses.filter((r) => r.ok).length + const failCount = responses.length - successCount + + if (successCount === responses.length) { + toast.success(`${successCount} tasks created successfully!`) + } else if (successCount > 0) { + toast.warning(`${successCount} tasks created, ${failCount} failed`) + } else { + toast.error('Failed to create tasks') + } + + // Refresh sidebar to get the real task data from server + await refreshTasks() + } catch (error) { + console.error('Error creating tasks:', error) + toast.error('Failed to create tasks') + await refreshTasks() + } finally { + setIsSubmitting(false) + } + } else { + // Single task creation (original behavior) + const { id } = addTaskOptimistically(data) + + // Navigate to the new task page immediately + router.push(`/tasks/${id}`) + + try { + const response = await fetch('/api/tasks', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ ...data, id }), // Include the pre-generated ID + }) + + if (response.ok) { + toast.success('Task created successfully!') + // Refresh sidebar to get the real task data from server + await refreshTasks() + } else { + const error = await response.json() + // Show detailed message for rate limits, or generic error message + toast.error(error.message || error.error || 'Failed to create task') + // TODO: Remove the optimistic task on error + await refreshTasks() // For now, just refresh to remove the optimistic task + } + } catch (error) { + console.error('Error creating task:', error) + toast.error('Failed to create task') + // TODO: Remove the optimistic task on error + await refreshTasks() // For now, just refresh to remove the optimistic task + } finally { + setIsSubmitting(false) + } + } + } + + const handleVercelSignIn = async () => { + setLoadingVercel(true) + await redirectToSignIn() + } + + const handleGitHubSignIn = () => { + setLoadingGitHub(true) + window.location.href = '/api/auth/signin/github' + } + + return ( +
+
+ +
+ +
+ +
+ + {/* Mobile Footer with Stars and Deploy Button - Only show when logged in */} + {user && } + + {/* Sign In Dialog */} + + + + Sign in to continue + + {hasGitHub && hasVercel + ? 'You need to sign in to create tasks. Choose how you want to sign in.' + : hasVercel + ? 'You need to sign in with Vercel to create tasks.' + : 'You need to sign in with GitHub to create tasks.'} + + + +
+ {hasVercel && ( + + )} + + {hasGitHub && ( + + )} +
+
+
+
+ ) +} From 35949bf204a848de8365b283d1a0395417b750a6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 15 Nov 2025 01:54:24 +0000 Subject: [PATCH 2/2] Show owner/repo dropdowns when logged out; place stars/deploy at bottom --- components/chat-page-content.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/chat-page-content.tsx b/components/chat-page-content.tsx index 7723a349..5df7f368 100644 --- a/components/chat-page-content.tsx +++ b/components/chat-page-content.tsx @@ -244,8 +244,8 @@ export function ChatPageContent({ /> - {/* Mobile Footer with Stars and Deploy Button - Only show when logged in */} - {user && } + {/* Mobile Footer with Stars and Deploy Button */} + {/* Sign In Dialog */}