From 81619c6e2a23e701c46db60e9e4b94f2b02a13f6 Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:06:47 +0800 Subject: [PATCH] refactor: converge app on Sealos codex flow --- README.md | 355 ++++-------- app/layout.tsx | 3 +- components/home-page-content.tsx | 677 ----------------------- components/home-page-mobile-footer.tsx | 31 -- components/multi-repo-dialog.tsx | 229 -------- components/open-repo-url-dialog.tsx | 88 --- components/repo-issues.tsx | 116 +--- components/repo-pull-requests.tsx | 116 +--- components/revert-commit-dialog.tsx | 119 +--- components/task-details.tsx | 131 +---- drizzle.config.ts | 18 +- lib/atoms/multi-repo.ts | 14 - lib/db/migrations/0028_solid_lockjaw.sql | 1 + lib/db/migrations/meta/_journal.json | 9 +- lib/db/schema.ts | 4 +- reference/architecture.md | 244 ++++++++ reference/configuration.md | 175 ++++++ 17 files changed, 574 insertions(+), 1756 deletions(-) delete mode 100644 components/home-page-content.tsx delete mode 100644 components/home-page-mobile-footer.tsx delete mode 100644 components/multi-repo-dialog.tsx delete mode 100644 components/open-repo-url-dialog.tsx delete mode 100644 lib/atoms/multi-repo.ts create mode 100644 lib/db/migrations/0028_solid_lockjaw.sql create mode 100644 reference/architecture.md create mode 100644 reference/configuration.md diff --git a/README.md b/README.md index e24fb07..931fe96 100644 --- a/README.md +++ b/README.md @@ -1,315 +1,154 @@ -# Coding Agent Template +# Analyze and Ship to Sealos -A template for building AI-powered coding agents that supports Claude Code, OpenAI's Codex CLI, GitHub Copilot CLI, Cursor CLI, Google Gemini CLI, and opencode with a Devbox runtime to automatically execute coding tasks on your repositories. +Analyze and Ship to Sealos is a Next.js app for running Sealos-focused coding and deployment tasks against GitHub repositories. A user selects a repo, submits a command, and the app runs a fixed `codex` + `gpt-5.4` execution path inside a Devbox runtime while surfacing logs, diffs, runtime state, and preview links in the UI. -![Coding Agent Template Screenshot](screenshot.png) +![Analyze and Ship to Sealos Screenshot](screenshot.png) -## Features +## Why This Project Exists -- **Multi-Agent Support**: Choose from Claude Code, OpenAI Codex CLI, GitHub Copilot CLI, Cursor CLI, Google Gemini CLI, or opencode to execute coding tasks -- **User Authentication**: Secure sign-in with GitHub OAuth -- **Multi-User Support**: Each user has their own tasks, API keys, and GitHub connection -- **Devbox Runtime**: Runs code in isolated, resumable runtimes -- **AI Gateway Integration**: Built for model routing and observability -- **AI-Generated Branch Names**: Automatically generates descriptive Git branch names using AI SDK 5 + AI Gateway -- **Task Management**: Track task progress with real-time updates -- **Persistent Storage**: Tasks stored in Neon Postgres database -- **Git Integration**: Automatically creates branches and commits changes -- **Modern UI**: Clean, responsive interface built with Next.js and Tailwind CSS -- **MCP Server Support**: Connect MCP servers to Claude Code for extended capabilities (Claude only) +- Turn a GitHub repository and a deployment-oriented prompt into a tracked task. +- Keep Sealos deployment work on a single, opinionated execution path instead of a generic agent router. +- Combine runtime management, chat, file changes, preview controls, and repository context in one app. +- Support per-user GitHub authentication, API keys, and connectors. -## Quick Start - -For detailed setup instructions, see the [Local Development Setup](#local-development-setup) section below. - -**TL;DR:** -1. Clone the repository and install dependencies -2. Configure GitHub OAuth and required environment variables -3. Initialize the database and start creating tasks - -Or run locally: -```bash -git clone -cd coding-agent-template -pnpm install -# Set up .env.local with required variables -pnpm db:push -pnpm dev -``` - -## Usage - -1. **Sign In**: Authenticate with GitHub -2. **Create a Task**: Enter a repository URL and describe what you want the AI to do -3. **Monitor Progress**: Watch real-time logs as the agent works -4. **Review Results**: See the changes made and the branch created -5. **Manage Tasks**: View all your tasks in the sidebar with status updates - -## Task Configuration - -### Maximum Duration - -The maximum duration setting controls how long the runtime sandbox will stay alive from the moment it's created. You can select timeouts ranging from 5 minutes to 5 hours. - -- The sandbox is created at the start of the task -- The timeout begins when the sandbox is created -- All work (agent execution, dependency installation, etc.) happens within this timeframe -- When the timeout is reached, the sandbox automatically expires - -### Keep Alive Setting - -The Keep Alive setting determines what happens to the sandbox after your task completes. - -#### Keep Alive OFF (Default) - -When Keep Alive is disabled, the sandbox shuts down immediately after the task completes: +## What Developers Get -**Timeline:** -1. Task starts and sandbox is created (e.g., with 1 hour timeout) -2. Agent executes your task -3. Task completes successfully (e.g., after 10 minutes) -4. Changes are committed and pushed to the branch -5. Sandbox immediately shuts down (destroys all processes and the environment) -6. Task is marked as completed - -**Use Keep Alive OFF when:** -- You're making one-time code changes that don't require iteration -- You have simple tasks that work on the first try -- You want to minimize resource usage and costs -- You don't need to test or manually interact with the code after completion - -#### Keep Alive ON - -When Keep Alive is enabled, the sandbox stays alive after task completion for the remaining duration: - -**Timeline:** -1. Task starts and sandbox is created (e.g., with 1 hour timeout) -2. Agent executes your task -3. Task completes successfully (e.g., after 10 minutes) -4. Changes are committed and pushed to the branch -5. Sandbox stays alive with all processes running -6. You can send follow-up messages for 50 more minutes (until the 1 hour timeout expires) -7. If the project has a dev server (e.g., `npm run dev`), it automatically starts in the background -8. After the full timeout duration, the sandbox expires - -**Use Keep Alive ON when:** -- You need to iterate on the code with follow-up messages -- You want to test changes in the live sandbox environment -- You anticipate needing to refine or fix issues -- You want to manually run commands or inspect the environment after completion -- You're developing a web application and want to see it running - -#### Comparison - -| Setting | Task completes in 10 min | Remaining sandbox time | Can send follow-ups? | Dev server starts? | -|---------|-------------------------|------------------------|---------------------|-------------------| -| Keep Alive ON | Sandbox stays alive | 50 minutes (until timeout) | Yes | Yes (if available) | -| Keep Alive OFF | Sandbox shuts down | 0 minutes | No | No | - -**Note:** The maximum duration timeout always takes precedence. If you set a 1-hour timeout, the sandbox will expire after 1 hour regardless of the Keep Alive setting. Keep Alive only determines whether the sandbox shuts down early (after task completion) or stays alive until the timeout. - -## How It Works - -1. **Task Creation**: When you submit a task, it's stored in the database -2. **AI Branch Name Generation**: AI SDK 5 + AI Gateway automatically generates a descriptive branch name based on your task (non-blocking using Next.js 15's `after()`) -3. **Sandbox Setup**: A Devbox runtime is created with your repository -4. **Agent Execution**: Your chosen coding agent (Claude Code, Codex CLI, GitHub Copilot CLI, Cursor CLI, Gemini CLI, or opencode) analyzes your prompt and makes changes -5. **Git Operations**: Changes are committed and pushed to the AI-generated branch -6. **Cleanup**: The sandbox is shut down to free resources - -## AI Branch Name Generation - -The system automatically generates descriptive Git branch names using AI SDK 5 and AI Gateway. This feature: - -- **Non-blocking**: Uses Next.js 15's `after()` function to generate names without delaying task creation -- **Descriptive**: Creates meaningful branch names like `feature/user-authentication-A1b2C3` or `fix/memory-leak-parser-X9y8Z7` -- **Conflict-free**: Adds a 6-character alphanumeric hash to prevent naming conflicts -- **Fallback**: Gracefully falls back to timestamp-based names if AI generation fails -- **Context-aware**: Uses task description, repository name, and agent context for better names - -### Branch Name Examples - -- `feature/add-user-auth-K3mP9n` (for "Add user authentication with JWT") -- `fix/resolve-memory-leak-B7xQ2w` (for "Fix memory leak in image processing") -- `chore/update-deps-M4nR8s` (for "Update all project dependencies") -- `docs/api-endpoints-F9tL5v` (for "Document REST API endpoints") +- A home command surface for starting Sealos deployment tasks. +- Task pages with logs, chat, file browsing, diff inspection, runtime controls, and preview actions. +- Repository pages for commits, issues, and pull requests. +- Persistent task state in Postgres through Drizzle ORM. +- Codex Gateway orchestration layered on top of Devbox runtime lifecycle management. ## Tech Stack -- **Frontend**: Next.js 15, React 19, Tailwind CSS -- **UI Components**: shadcn/ui -- **Database**: PostgreSQL with Drizzle ORM -- **AI SDK**: AI SDK 5 -- **AI Agents**: Claude Code, OpenAI Codex CLI, GitHub Copilot CLI, Cursor CLI, Google Gemini CLI, opencode -- **Runtime**: Devbox-based execution environment -- **Authentication**: OAuth with GitHub -- **Git**: Automated branching and commits with AI-generated branch names - -## MCP Server Support - -Connect MCP Servers to extend Claude Code with additional tools and integrations. **Currently only works with Claude Code agent.** - -### How to Add MCP Servers +- Next.js 16 +- React 19 +- Tailwind CSS +- shadcn/ui +- PostgreSQL +- Drizzle ORM and drizzle-kit +- AI SDK 5 +- Codex Gateway +- Devbox runtime infrastructure -1. Go to the "Connectors" tab and click "Add MCP Server" -2. Enter server details (name, base URL, optional OAuth credentials) -3. If using OAuth, ensure `ENCRYPTION_KEY` is set in your environment variables +## Quick Start -**Note**: `ENCRYPTION_KEY` is required when using MCP servers with OAuth authentication. +### Prerequisites -## Local Development Setup +- Node.js 20+ +- pnpm 9+ +- A PostgreSQL database +- A GitHub OAuth app +- Access to the Sealos and Devbox environment this app targets +- An AI gateway key for Codex execution, unless you plan to provide user-scoped keys in the app -### 1. Clone the repository +### 1. Install dependencies ```bash git clone -cd coding-agent-template -``` - -### 2. Install dependencies - -```bash +cd ShipRepo pnpm install ``` -### 3. Set up environment variables - -Create a `.env.local` file with your values: - -#### Required Environment Variables (App Infrastructure) - -These are set once by you (the app developer) and are used for core infrastructure: - -- `POSTGRES_URL`: Your PostgreSQL connection string -- `DEVBOX_TOKEN`: Token used to provision and access the Devbox runtime API -- `SEALOS_HOST`: Sealos host entrypoint, for example `staging-usw-1.sealos.io` -- `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`) - -The app derives the rest from `SEALOS_HOST`: +### 2. Configure environment variables -- `region`: `staging-usw-1` -- `region_url`: `https://staging-usw-1.sealos.io` -- `template_api`: `https://template.staging-usw-1.sealos.io/api/v2alpha/templates/raw` -- `devbox_base_url`: `https://devbox-server.staging-usw-1.sealos.io` - -#### User Authentication (Required) - -Configure GitHub OAuth for user authentication: +Create `.env.local`: ```bash +POSTGRES_URL= +SEALOS_HOST= +DEVBOX_TOKEN= +JWE_SECRET= +ENCRYPTION_KEY= NEXT_PUBLIC_AUTH_PROVIDERS=github +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +AI_GATEWAY_API_KEY= ``` -Required GitHub OAuth variables: -- `GITHUB_CLIENT_ID`: Your GitHub OAuth app client ID -- `GITHUB_CLIENT_SECRET`: Your GitHub OAuth app client secret -- `APP_BASE_URL`: Optional explicit public app URL for OAuth callbacks in self-hosted deployments - -#### API Keys (Optional - Can be per-user) - -These API keys can be set globally (fallback for all users) or left unset to require users to provide their own: - -- `ANTHROPIC_API_KEY`: Anthropic API key for Claude agent (users can override in their profile) -- `AI_GATEWAY_API_KEY`: AI Gateway API key for branch name generation and Codex (users can override) -- `CURSOR_API_KEY`: For Cursor agent support (users can override) -- `GEMINI_API_KEY`: For Google Gemini agent support (users can override) -- `OPENAI_API_KEY`: For Codex and OpenCode agents (users can override) +Optional: -> **Note**: Users can provide their own API keys in their profile settings, which take precedence over global environment variables. +- `APP_BASE_URL` for self-hosted callback overrides +- `NPM_TOKEN` for private npm installs inside task runtimes +- `MAX_SANDBOX_DURATION` to change the default runtime timeout +- `MAX_MESSAGES_PER_DAY` to change the per-user daily message limit -#### GitHub Repository Access +### 3. Apply database migrations -- ~~`GITHUB_TOKEN`~~: **No longer needed!** Users authenticate with their own GitHub accounts. - - Users who sign in with GitHub automatically get repository access via their OAuth token - -**How Authentication Works:** -- **Sign in with GitHub**: Users get immediate repository access via their GitHub OAuth token +```bash +pnpm db:migrate +``` -#### Optional Environment Variables +`drizzle.config.ts` loads `.env.local` first and falls back to `.env`, so `POSTGRES_URL` must be available before running Drizzle commands. -- `NPM_TOKEN`: For private npm packages -- `MAX_SANDBOX_DURATION`: Default maximum sandbox duration in minutes (default: `300` = 5 hours) -- `MAX_MESSAGES_PER_DAY`: Maximum number of tasks + follow-ups per user per day (default: `5`) +### 4. Start the app -### 4. Set up OAuth Applications +```bash +pnpm dev +``` -Based on your `NEXT_PUBLIC_AUTH_PROVIDERS` configuration, you'll need to create OAuth apps: +Open [http://localhost:3000](http://localhost:3000), sign in with GitHub, choose a repository, and submit a Sealos-oriented task. -#### GitHub OAuth App (if using GitHub authentication) +## Core Workflow -1. Go to [GitHub Developer Settings](https://github.com/settings/developers) -2. Click "New OAuth App" -3. Fill in the details: - - **Application name**: Your app name (e.g., "My Coding Agent") - - **Homepage URL**: `http://localhost:3000` (or your production URL) - - **Authorization callback URL**: `http://localhost:3000/api/auth/github/callback` -4. Click "Register application" -5. Copy the **Client ID** → use for `GITHUB_CLIENT_ID` -6. Click "Generate a new client secret" → copy and use for `GITHUB_CLIENT_SECRET` +1. A user signs in with GitHub. +2. The home page lets the user choose a repository and submit a deployment-oriented command. +3. The app creates a task, stores it in Postgres, and starts the fixed `codex` + `gpt-5.4` flow. +4. A Devbox runtime is provisioned or resumed for the task. +5. The task is executed through the Codex Gateway. +6. The task page shows logs, runtime state, file changes, preview actions, and follow-up chat. -**Required Scopes**: The app will request `repo`, `read:user`, `user:email`, `read:packages`, and `write:packages` scopes to access repositories and push container images to GHCR. +## Project Structure -> **Production Deployment**: Remember to add production callback URLs when deploying (e.g., `https://yourdomain.com/api/auth/github/callback`) +- `app/`: Next.js App Router pages and API routes +- `components/`: UI for the home page, task workspace, repo views, auth, and dialogs +- `lib/codex-gateway/`: Codex Gateway sessions, turns, streaming, and completion handling +- `lib/devbox/`: runtime provisioning, reuse, health checks, and lease refresh +- `lib/db/`: schema, queries, settings, and checked-in migrations +- `lib/session/` and `lib/auth/`: authentication and session handling +- `app/repos/[owner]/[repo]/`: repository pages for commits, issues, and pull requests +- `app/tasks/[taskId]/`: task workspace entry point -### 5. Set up the database +## Further Reading -Generate and run database migrations: +- [reference/architecture.md](reference/architecture.md): request flow, runtime lifecycle, and module boundaries +- [reference/configuration.md](reference/configuration.md): environment variables, auth setup, migrations, and runtime behavior -```bash -pnpm db:generate -pnpm db:push -``` +## Development -### 6. Start the development server +### Common commands ```bash pnpm dev +pnpm build +pnpm type-check +pnpm lint +pnpm format ``` -Open [http://localhost:3000](http://localhost:3000) in your browser. - -## Development - -### Database Operations +### Database commands ```bash -# Generate migrations pnpm db:generate - -# Push schema changes -pnpm db:push - -# Open Drizzle Studio +pnpm db:migrate pnpm db:studio ``` -### Running the App +## Configuration Notes -```bash -# Development -pnpm dev - -# Build for production -pnpm build - -# Start production server -pnpm start -``` +- The current task execution path is intentionally pinned to `codex` + `gpt-5.4`. +- `NEXT_PUBLIC_AUTH_PROVIDERS` is expected to include `github`. +- Users can provide their own API keys in the app, which can override global key configuration. +- Connectors are managed from the application UI; if a connector stores OAuth credentials, `ENCRYPTION_KEY` must be set. ## Contributing -1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Test thoroughly -5. Submit a pull request +1. Fork the repository. +2. Create a branch for your change. +3. Run `pnpm format`, `pnpm type-check`, and `pnpm lint`. +4. Verify the change locally. +5. Open a pull request. -## Security Considerations +## License -- **Environment Variables**: Never commit `.env` files to version control. All sensitive data should be stored in environment variables. -- **API Keys**: Rotate your API keys regularly and use the principle of least privilege. -- **Database Access**: Ensure your PostgreSQL database is properly secured with strong credentials. -- **Devbox Runtime**: Runtimes are isolated but ensure you're not exposing sensitive data in logs or outputs. -- **User Authentication**: Each user uses their own GitHub token for repository access - no shared credentials -- **Encryption**: All sensitive data (tokens, API keys) is encrypted at rest using per-user encryption +Licensed under Apache 2.0. See [LICENSE](LICENSE). diff --git a/app/layout.tsx b/app/layout.tsx index b15f68f..dd10d66 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -28,8 +28,7 @@ const commissioner = Commissioner({ export const metadata: Metadata = { title: 'Analyze and Ship to Sealos', - description: - 'AI-powered coding agent template supporting Claude Code, OpenAI Codex CLI, Cursor CLI, and opencode with a Devbox runtime', + description: 'Deploy repositories to Sealos with Codex, GPT-5.4, Devbox runtimes, and the Codex Gateway.', } export default function RootLayout({ diff --git a/components/home-page-content.tsx b/components/home-page-content.tsx deleted file mode 100644 index 05c14ef..0000000 --- a/components/home-page-content.tsx +++ /dev/null @@ -1,677 +0,0 @@ -'use client' - -import { useState, useEffect } from 'react' -import { TaskForm } from '@/components/task-form' -import { SharedHeader } from '@/components/shared-header' -import { RepoSelector } from '@/components/repo-selector' -import { toast } from 'sonner' -import { useRouter, useSearchParams } 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 { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - DropdownMenuSeparator, -} from '@/components/ui/dropdown-menu' -import { MoreHorizontal, RefreshCw, Unlink, Settings, Plus, ExternalLink } from 'lucide-react' -import { redirectToSignIn } from '@/lib/session/redirect-to-sign-in' -import { GitHubIcon } from '@/components/icons/github-icon' -import { getEnabledAuthProviders } from '@/lib/auth/providers' -import { useSetAtom, useAtom, useAtomValue } from 'jotai' -import { taskPromptAtom } from '@/lib/atoms/task' -import { HomePageMobileFooter } from '@/components/home-page-mobile-footer' -import { multiRepoModeAtom, selectedReposAtom } from '@/lib/atoms/multi-repo' -import { sessionAtom } from '@/lib/atoms/session' -import { githubConnectionAtom, githubConnectionInitializedAtom } from '@/lib/atoms/github-connection' -import { OpenRepoUrlDialog } from '@/components/open-repo-url-dialog' - -interface HomePageContentProps { - initialSelectedOwner?: string - initialSelectedRepo?: string - initialInstallDependencies?: boolean - initialMaxDuration?: number - initialKeepAlive?: boolean - initialEnableBrowser?: boolean - maxSandboxDuration?: number - user?: Session['user'] | null - initialStars?: number -} - -export function HomePageContent({ - initialSelectedOwner = '', - initialSelectedRepo = '', - initialInstallDependencies = false, - initialMaxDuration = 300, - initialKeepAlive = false, - initialEnableBrowser = false, - maxSandboxDuration = 300, - user = null, - initialStars = 1200, -}: HomePageContentProps) { - const [isSubmitting, setIsSubmitting] = useState(false) - const [selectedOwner, setSelectedOwnerState] = useState(initialSelectedOwner) - const [selectedRepo, setSelectedRepoState] = useState(initialSelectedRepo) - const [showSignInDialog, setShowSignInDialog] = useState(false) - const [loadingVercel, setLoadingVercel] = useState(false) - const [loadingGitHub, setLoadingGitHub] = useState(false) - const [isRefreshing, setIsRefreshing] = useState(false) - const [showOpenRepoDialog, setShowOpenRepoDialog] = useState(false) - const router = useRouter() - const searchParams = useSearchParams() - const { refreshTasks, addTaskOptimistically } = useTasks() - const setTaskPrompt = useSetAtom(taskPromptAtom) - - // Multi-repo mode state - const multiRepoMode = useAtomValue(multiRepoModeAtom) - const [selectedRepos, setSelectedRepos] = useAtom(selectedReposAtom) - - // GitHub connection state - const session = useAtomValue(sessionAtom) - const githubConnection = useAtomValue(githubConnectionAtom) - const githubConnectionInitialized = useAtomValue(githubConnectionInitializedAtom) - const setGitHubConnection = useSetAtom(githubConnectionAtom) - const isGitHubAuthUser = session.authProvider === 'github' - - // Check which auth providers are enabled - const { github: hasGitHub, vercel: hasVercel } = getEnabledAuthProviders() - - // Show toast if GitHub was connected (user was already logged in) - useEffect(() => { - if (searchParams.get('github_connected') === 'true') { - toast.success('GitHub account connected successfully!') - // Remove the query parameter from URL - const newUrl = new URL(window.location.href) - newUrl.searchParams.delete('github_connected') - window.history.replaceState({}, '', newUrl.toString()) - } - }, [searchParams]) - - // Check for newly created repo and select it - useEffect(() => { - const newlyCreatedRepo = localStorage.getItem('newly-created-repo') - if (newlyCreatedRepo) { - try { - const { owner, repo } = JSON.parse(newlyCreatedRepo) - if (owner && repo) { - // Set owner and repo directly without triggering the clear logic - setSelectedOwnerState(owner) - setSelectedOwner(owner) - setSelectedRepoState(repo) - setSelectedRepo(repo) - } - } catch (error) { - console.error('Error parsing newly created repo:', error) - } finally { - // Clear the localStorage item after using it - localStorage.removeItem('newly-created-repo') - } - } - }, []) // Run only on mount - - // Check for URL query parameters for owner and repo - useEffect(() => { - const urlOwner = searchParams.get('owner') - const urlRepo = searchParams.get('repo') - - if (urlOwner && urlOwner !== selectedOwner) { - setSelectedOwnerState(urlOwner) - setSelectedOwner(urlOwner) - } - if (urlRepo && urlRepo !== selectedRepo) { - setSelectedRepoState(urlRepo) - setSelectedRepo(urlRepo) - } - }, [searchParams, selectedOwner, selectedRepo]) - - // Wrapper functions to update both state and cookies - const handleOwnerChange = (owner: string) => { - setSelectedOwnerState(owner) - setSelectedOwner(owner) - // Clear repo when owner changes - if (selectedRepo) { - setSelectedRepoState('') - setSelectedRepo('') - } - } - - const handleRepoChange = (repo: string) => { - setSelectedRepoState(repo) - setSelectedRepo(repo) - } - - const handleRefreshOwners = () => { - setIsRefreshing(true) - localStorage.removeItem('github-owners') - toast.success('Refreshing owners...') - window.location.reload() - } - - const handleRefreshRepos = () => { - setIsRefreshing(true) - if (selectedOwner) { - localStorage.removeItem(`github-repos-${selectedOwner}`) - toast.success('Refreshing repositories...') - } else { - Object.keys(localStorage).forEach((key) => { - if (key.startsWith('github-repos-')) { - localStorage.removeItem(key) - } - }) - toast.success('Refreshing all repositories...') - } - window.location.reload() - } - - const handleDisconnectGitHub = async () => { - try { - const response = await fetch('/api/auth/github/disconnect', { - method: 'POST', - credentials: 'include', - }) - - if (response.ok) { - toast.success('GitHub disconnected') - localStorage.removeItem('github-owners') - Object.keys(localStorage).forEach((key) => { - if (key.startsWith('github-repos-')) { - localStorage.removeItem(key) - } - }) - handleOwnerChange('') - handleRepoChange('') - setGitHubConnection({ connected: false }) - router.refresh() - } else { - const error = await response.json() - console.error('Failed to disconnect GitHub:', error) - toast.error(error.error || 'Failed to disconnect GitHub') - } - } catch (error) { - console.error('Failed to disconnect GitHub:', error) - toast.error('Failed to disconnect GitHub') - } - } - - const handleNewRepo = () => { - const url = selectedOwner ? `/repos/new?owner=${selectedOwner}` : '/repos/new' - router.push(url) - } - - const handleConnectGitHub = () => { - window.location.href = '/api/auth/github/signin' - } - - const handleReconfigureGitHub = () => { - window.open('https://github.com/settings/connections/applications', '_blank') - } - - const handleOpenRepoUrl = async (repoUrl: string) => { - try { - if (!user) { - toast.error('Sign in required', { - description: 'Please sign in to create tasks with custom repository URLs.', - }) - return - } - - const taskData = { - prompt: 'Work on this repository', - repoUrl: repoUrl, - selectedAgent: localStorage.getItem('last-selected-agent') || 'claude', - selectedModel: localStorage.getItem('last-selected-model-claude') || 'claude-sonnet-4-5', - installDependencies: true, - maxDuration: 300, - keepAlive: false, - } - - const { id } = addTaskOptimistically(taskData) - router.push(`/tasks/${id}`) - - const response = await fetch('/api/tasks', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ ...taskData, id }), - }) - - if (response.ok) { - toast.success('Task created successfully!') - } else { - const error = await response.json() - toast.error(error.message || error.error || 'Failed to create task') - } - } catch (error) { - console.error('Error creating task:', error) - toast.error('Failed to create task') - } - } - - // Build leftActions for the header - const headerLeftActions = ( -
- {!githubConnectionInitialized ? null : githubConnection.connected || isGitHubAuthUser ? ( - <> - - - - - - - - - New Repo - - setShowOpenRepoDialog(true)}> - - Open Repo URL - - - - - Refresh Owners - - - - Refresh Repos - - - - Manage Access - - {!isGitHubAuthUser && ( - - - Disconnect GitHub - - )} - - - - ) : user ? ( - - ) : selectedOwner || selectedRepo ? ( - - ) : null} -
- ) - - const handleTaskSubmit = async (data: { - prompt: string - repoUrl: string - selectedAgent: string - selectedModel: string - selectedModels?: string[] - installDependencies: boolean - maxDuration: number - keepAlive: boolean - enableBrowser: boolean - }) => { - // Check if user is authenticated - if (!user) { - setShowSignInDialog(true) - return - } - - // Check if multi-repo mode is enabled - if (multiRepoMode) { - if (selectedRepos.length === 0) { - toast.error('Please select repositories', { - description: 'Click on "0 repos selected" to choose repositories.', - }) - return - } - } else { - // 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-repo mode - if (multiRepoMode && selectedRepos.length > 0) { - // Create multiple tasks, one for each selected repo - const taskIds: string[] = [] - const tasksData = selectedRepos.map((repo) => { - const { id } = addTaskOptimistically({ - prompt: data.prompt, - repoUrl: repo.clone_url, - selectedAgent: data.selectedAgent, - selectedModel: data.selectedModel, - installDependencies: data.installDependencies, - maxDuration: data.maxDuration, - }) - taskIds.push(id) - return { - id, - prompt: data.prompt, - repoUrl: repo.clone_url, - selectedAgent: data.selectedAgent, - selectedModel: data.selectedModel, - installDependencies: data.installDependencies, - maxDuration: data.maxDuration, - keepAlive: data.keepAlive, - enableBrowser: data.enableBrowser, - } - }) - - // 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') - } - - // Clear selected repos after creating tasks - setSelectedRepos([]) - - // 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) - } - return - } - - // 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, - enableBrowser: data.enableBrowser, - } - }) - - // 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 - Show when logged in OR when owner/repo are selected */} - {(user || selectedOwner || selectedRepo) && } - - {/* Dialogs */} - - - {/* 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 && ( - - )} -
-
-
-
- ) -} diff --git a/components/home-page-mobile-footer.tsx b/components/home-page-mobile-footer.tsx deleted file mode 100644 index 9441aac..0000000 --- a/components/home-page-mobile-footer.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client' - -import { Button } from '@/components/ui/button' -import { GitHubIcon } from '@/components/icons/github-icon' -import { formatAbbreviatedNumber } from '@/lib/utils/format-number' - -const GITHUB_REPO_URL = 'https://github.com/vercel-labs/coding-agent-template' - -interface HomePageMobileFooterProps { - initialStars?: number -} - -export function HomePageMobileFooter({ initialStars = 1200 }: HomePageMobileFooterProps) { - return ( -
-
- -
-
- ) -} diff --git a/components/multi-repo-dialog.tsx b/components/multi-repo-dialog.tsx deleted file mode 100644 index e49acc3..0000000 --- a/components/multi-repo-dialog.tsx +++ /dev/null @@ -1,229 +0,0 @@ -'use client' - -import { useState, useEffect, useRef, useMemo } from 'react' -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog' -import { Input } from '@/components/ui/input' -import { Button } from '@/components/ui/button' -import { Badge } from '@/components/ui/badge' -import { X, Search, Lock, Loader2 } from 'lucide-react' -import { useAtom, useAtomValue } from 'jotai' -import { selectedReposAtom, type SelectedRepo } from '@/lib/atoms/multi-repo' -import { githubOwnersAtom } from '@/lib/atoms/github-cache' - -interface GitHubRepo { - name: string - full_name: string - description: string - private: boolean - clone_url: string - language: string -} - -interface RepoWithOwner extends GitHubRepo { - owner: string -} - -interface MultiRepoDialogProps { - open: boolean - onOpenChange: (open: boolean) => void -} - -export function MultiRepoDialog({ open, onOpenChange }: MultiRepoDialogProps) { - const [selectedRepos, setSelectedRepos] = useAtom(selectedReposAtom) - const owners = useAtomValue(githubOwnersAtom) - const [searchQuery, setSearchQuery] = useState('') - const [allRepos, setAllRepos] = useState([]) - const [loadingRepos, setLoadingRepos] = useState(false) - const [showDropdown, setShowDropdown] = useState(false) - const inputRef = useRef(null) - const dropdownRef = useRef(null) - - // Load repos from all owners when dialog opens - useEffect(() => { - if (open && owners && owners.length > 0 && allRepos.length === 0) { - const loadAllRepos = async () => { - setLoadingRepos(true) - try { - const repoPromises = owners.map(async (owner) => { - try { - const response = await fetch(`/api/github/repos?owner=${owner.login}`) - if (response.ok) { - const repos: GitHubRepo[] = await response.json() - return repos.map((repo) => ({ ...repo, owner: owner.login })) - } - } catch (error) { - console.error('Error loading repos for owner:', error) - } - return [] - }) - - const results = await Promise.all(repoPromises) - const combinedRepos = results.flat() - setAllRepos(combinedRepos) - } catch (error) { - console.error('Error loading repos:', error) - } finally { - setLoadingRepos(false) - } - } - loadAllRepos() - } - }, [open, owners, allRepos.length]) - - // Filter repos based on search query and exclude already selected repos - const filteredRepos = useMemo(() => { - if (!allRepos.length) return [] - - const query = searchQuery.toLowerCase() - return allRepos.filter( - (repo) => - // Match search query against full_name, name, or description - (repo.full_name.toLowerCase().includes(query) || - repo.name.toLowerCase().includes(query) || - repo.description?.toLowerCase().includes(query)) && - // Exclude already selected repos - !selectedRepos.some((r) => r.full_name === repo.full_name), - ) - }, [allRepos, searchQuery, selectedRepos]) - - // Handle repo selection - const handleSelectRepo = (repo: RepoWithOwner) => { - const newRepo: SelectedRepo = { - owner: repo.owner, - repo: repo.name, - full_name: repo.full_name, - clone_url: repo.clone_url, - } - - setSelectedRepos([...selectedRepos, newRepo]) - setSearchQuery('') - setShowDropdown(false) - inputRef.current?.focus() - } - - // Handle repo removal - const handleRemoveRepo = (fullName: string) => { - setSelectedRepos(selectedRepos.filter((r) => r.full_name !== fullName)) - } - - // Close dropdown when clicking outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) && - inputRef.current && - !inputRef.current.contains(event.target as Node) - ) { - setShowDropdown(false) - } - } - - document.addEventListener('mousedown', handleClickOutside) - return () => document.removeEventListener('mousedown', handleClickOutside) - }, []) - - return ( - - - - Select Repositories - - Choose multiple repositories to create tasks for. A separate task will be created for each selected - repository. - - - -
- {/* Selected repos - shown above search so dropdown doesn't cover them */} - {selectedRepos.length > 0 && ( -
-
- Selected ({selectedRepos.length}): - -
-
- {selectedRepos.map((repo) => ( - - {repo.full_name} - - - ))} -
-
- )} - - {/* Search input */} -
- - { - setSearchQuery(e.target.value) - setShowDropdown(true) - }} - onFocus={() => setShowDropdown(true)} - className="pl-9" - /> - - {/* Dropdown */} - {showDropdown && ( -
- {loadingRepos ? ( -
- - Loading repositories... -
- ) : filteredRepos.length === 0 ? ( -
- {searchQuery ? `No repositories match "${searchQuery}"` : 'No repositories found'} -
- ) : ( - filteredRepos.slice(0, 50).map((repo) => ( - - )) - )} - {filteredRepos.length > 50 && ( -
- Showing first 50 of {filteredRepos.length} repositories. Use search to find more. -
- )} -
- )} -
-
- -
- - -
-
-
- ) -} diff --git a/components/open-repo-url-dialog.tsx b/components/open-repo-url-dialog.tsx deleted file mode 100644 index 1e28fe7..0000000 --- a/components/open-repo-url-dialog.tsx +++ /dev/null @@ -1,88 +0,0 @@ -'use client' - -import { useState } from 'react' -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Button } from '@/components/ui/button' -import { toast } from 'sonner' - -interface OpenRepoUrlDialogProps { - open: boolean - onOpenChange: (open: boolean) => void - onSubmit: (repoUrl: string) => void -} - -export function OpenRepoUrlDialog({ open, onOpenChange, onSubmit }: OpenRepoUrlDialogProps) { - const [repoUrl, setRepoUrl] = useState('') - const [isSubmitting, setIsSubmitting] = useState(false) - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - - if (!repoUrl.trim()) { - toast.error('Please enter a repository URL') - return - } - - // Basic validation for GitHub URL format - const githubUrlPattern = /^https?:\/\/(www\.)?github\.com\/[\w-]+\/[\w.-]+/ - if (!githubUrlPattern.test(repoUrl.trim())) { - toast.error('Invalid GitHub repository URL', { - description: 'Please enter a valid GitHub repository URL (e.g., https://github.com/owner/repo)', - }) - return - } - - setIsSubmitting(true) - try { - onSubmit(repoUrl.trim()) - // Reset form - setRepoUrl('') - onOpenChange(false) - } catch (error) { - console.error('Error processing repo URL:', error) - toast.error('Failed to process repository URL') - } finally { - setIsSubmitting(false) - } - } - - return ( - - - - Open Repository URL - - Enter a GitHub repository URL to create a task. The repository will be cloned and you can start working on - it. - - - -
-
- - setRepoUrl(e.target.value)} - disabled={isSubmitting} - className="w-full" - /> -
- -
- - -
-
-
-
- ) -} diff --git a/components/repo-issues.tsx b/components/repo-issues.tsx index d943665..07019a0 100644 --- a/components/repo-issues.tsx +++ b/components/repo-issues.tsx @@ -21,69 +21,8 @@ import { Checkbox } from '@/components/ui/checkbox' import { Label } from '@/components/ui/label' import { User, Calendar, MessageSquare, MoreVertical, ListTodo } from 'lucide-react' import { toast } from 'sonner' -import Claude from '@/components/logos/claude' -import Codex from '@/components/logos/codex' -import Copilot from '@/components/logos/copilot' -import Cursor from '@/components/logos/cursor' -import Gemini from '@/components/logos/gemini' -import OpenCode from '@/components/logos/opencode' - -const CODING_AGENTS = [ - { value: 'claude', label: 'Claude', icon: Claude }, - { value: 'codex', label: 'Codex', icon: Codex }, - { value: 'copilot', label: 'Copilot', icon: Copilot }, - { value: 'cursor', label: 'Cursor', icon: Cursor }, - { value: 'gemini', label: 'Gemini', icon: Gemini }, - { value: 'opencode', label: 'opencode', icon: OpenCode }, -] as const - -const AGENT_MODELS = { - claude: [ - { value: 'claude-sonnet-4-5', label: 'Sonnet 4.5' }, - { value: 'anthropic/claude-opus-4.6', label: 'Opus 4.6' }, - { value: 'claude-haiku-4-5', label: 'Haiku 4.5' }, - ], - codex: [{ value: 'gpt-5.4', label: 'GPT-5.4' }], - copilot: [ - { value: 'claude-sonnet-4.5', label: 'Sonnet 4.5' }, - { value: 'claude-sonnet-4', label: 'Sonnet 4' }, - { value: 'claude-haiku-4.5', label: 'Haiku 4.5' }, - { value: 'gpt-5', label: 'GPT-5' }, - ], - cursor: [ - { value: 'auto', label: 'Auto' }, - { value: 'composer-1', label: 'Composer' }, - { value: 'sonnet-4.5', label: 'Sonnet 4.5' }, - { value: 'sonnet-4.5-thinking', label: 'Sonnet 4.5 Thinking' }, - { value: 'gpt-5', label: 'GPT-5' }, - { value: 'gpt-5-codex', label: 'GPT-5 Codex' }, - { value: 'opus-4.1', label: 'Opus 4.1' }, - { value: 'grok', label: 'Grok' }, - ], - gemini: [ - { value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' }, - { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' }, - { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' }, - ], - opencode: [ - { value: 'gpt-5', label: 'GPT-5' }, - { value: 'gpt-5-mini', label: 'GPT-5 mini' }, - { value: 'gpt-5-nano', label: 'GPT-5 nano' }, - { value: 'gpt-4.1', label: 'GPT-4.1' }, - { value: 'claude-sonnet-4-5', label: 'Sonnet 4.5' }, - { value: 'claude-opus-4-5', label: 'Opus 4.5' }, - { value: 'claude-haiku-4-5', label: 'Haiku 4.5' }, - ], -} as const - -const DEFAULT_MODELS = { - claude: 'claude-sonnet-4-5', - codex: 'gpt-5.4', - copilot: 'claude-sonnet-4.5', - cursor: 'auto', - gemini: 'gemini-3-pro-preview', - opencode: 'gpt-5', -} as const +const FIXED_TASK_AGENT = 'codex' +const FIXED_TASK_MODEL = 'gpt-5.4' function formatDistanceToNow(date: Date): string { const now = new Date() @@ -142,8 +81,6 @@ export function RepoIssues({ owner, repo }: RepoIssuesProps) { const [error, setError] = useState(null) const [showCreateTaskDialog, setShowCreateTaskDialog] = useState(false) const [selectedIssue, setSelectedIssue] = useState(null) - const [selectedAgent, setSelectedAgent] = useState('claude') - const [selectedModel, setSelectedModel] = useState(DEFAULT_MODELS.claude) const [installDeps, setInstallDeps] = useState(false) const [maxDuration, setMaxDuration] = useState(300) const [keepAlive, setKeepAlive] = useState(false) @@ -170,14 +107,6 @@ export function RepoIssues({ owner, repo }: RepoIssuesProps) { fetchIssues() }, [owner, repo]) - useEffect(() => { - const agentModels = AGENT_MODELS[selectedAgent as keyof typeof AGENT_MODELS] - const defaultModel = DEFAULT_MODELS[selectedAgent as keyof typeof DEFAULT_MODELS] - if (agentModels && !agentModels.find((m) => m.value === selectedModel)) { - setSelectedModel(defaultModel) - } - }, [selectedAgent, selectedModel]) - const handleCreateTaskFromIssue = (issue: Issue) => { setSelectedIssue(issue) setShowCreateTaskDialog(true) @@ -199,8 +128,8 @@ export function RepoIssues({ owner, repo }: RepoIssuesProps) { body: JSON.stringify({ prompt, repoUrl, - selectedAgent, - selectedModel, + selectedAgent: FIXED_TASK_AGENT, + selectedModel: FIXED_TASK_MODEL, installDependencies: installDeps, maxDuration, keepAlive, @@ -344,40 +273,9 @@ export function RepoIssues({ owner, repo }: RepoIssuesProps) {
-
-
- - -
-
- - -
+
+ This task will run with Codex on{' '} + GPT-5.4.
{/* Task Options */} diff --git a/components/repo-pull-requests.tsx b/components/repo-pull-requests.tsx index f4415fd..0a86ac2 100644 --- a/components/repo-pull-requests.tsx +++ b/components/repo-pull-requests.tsx @@ -29,69 +29,8 @@ import { Checkbox } from '@/components/ui/checkbox' import { Label } from '@/components/ui/label' import { GitPullRequest, Calendar, MessageSquare, MoreHorizontal, X, ListTodo } from 'lucide-react' import { toast } from 'sonner' -import Claude from '@/components/logos/claude' -import Codex from '@/components/logos/codex' -import Copilot from '@/components/logos/copilot' -import Cursor from '@/components/logos/cursor' -import Gemini from '@/components/logos/gemini' -import OpenCode from '@/components/logos/opencode' - -const CODING_AGENTS = [ - { value: 'claude', label: 'Claude', icon: Claude }, - { value: 'codex', label: 'Codex', icon: Codex }, - { value: 'copilot', label: 'Copilot', icon: Copilot }, - { value: 'cursor', label: 'Cursor', icon: Cursor }, - { value: 'gemini', label: 'Gemini', icon: Gemini }, - { value: 'opencode', label: 'opencode', icon: OpenCode }, -] as const - -const AGENT_MODELS = { - claude: [ - { value: 'claude-sonnet-4-5', label: 'Sonnet 4.5' }, - { value: 'anthropic/claude-opus-4.6', label: 'Opus 4.6' }, - { value: 'claude-haiku-4-5', label: 'Haiku 4.5' }, - ], - codex: [{ value: 'gpt-5.4', label: 'GPT-5.4' }], - copilot: [ - { value: 'claude-sonnet-4.5', label: 'Sonnet 4.5' }, - { value: 'claude-sonnet-4', label: 'Sonnet 4' }, - { value: 'claude-haiku-4.5', label: 'Haiku 4.5' }, - { value: 'gpt-5', label: 'GPT-5' }, - ], - cursor: [ - { value: 'auto', label: 'Auto' }, - { value: 'composer-1', label: 'Composer' }, - { value: 'sonnet-4.5', label: 'Sonnet 4.5' }, - { value: 'sonnet-4.5-thinking', label: 'Sonnet 4.5 Thinking' }, - { value: 'gpt-5', label: 'GPT-5' }, - { value: 'gpt-5-codex', label: 'GPT-5 Codex' }, - { value: 'opus-4.1', label: 'Opus 4.1' }, - { value: 'grok', label: 'Grok' }, - ], - gemini: [ - { value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' }, - { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' }, - { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' }, - ], - opencode: [ - { value: 'gpt-5', label: 'GPT-5' }, - { value: 'gpt-5-mini', label: 'GPT-5 mini' }, - { value: 'gpt-5-nano', label: 'GPT-5 nano' }, - { value: 'gpt-4.1', label: 'GPT-4.1' }, - { value: 'claude-sonnet-4-5', label: 'Sonnet 4.5' }, - { value: 'claude-opus-4-5', label: 'Opus 4.5' }, - { value: 'claude-haiku-4-5', label: 'Haiku 4.5' }, - ], -} as const - -const DEFAULT_MODELS = { - claude: 'claude-sonnet-4-5', - codex: 'gpt-5.4', - copilot: 'claude-sonnet-4.5', - cursor: 'auto', - gemini: 'gemini-3-pro-preview', - opencode: 'gpt-5', -} as const +const FIXED_TASK_AGENT = 'codex' +const FIXED_TASK_MODEL = 'gpt-5.4' function formatDistanceToNow(date: Date): string { const now = new Date() @@ -138,8 +77,6 @@ export function RepoPullRequests({ owner, repo }: RepoPullRequestsProps) { const [showCloseDialog, setShowCloseDialog] = useState(false) const [selectedPR, setSelectedPR] = useState(null) const [prToClose, setPrToClose] = useState(null) - const [selectedAgent, setSelectedAgent] = useState('claude') - const [selectedModel, setSelectedModel] = useState(DEFAULT_MODELS.claude) const [installDeps, setInstallDeps] = useState(false) const [maxDuration, setMaxDuration] = useState(300) const [keepAlive, setKeepAlive] = useState(false) @@ -189,14 +126,6 @@ export function RepoPullRequests({ owner, repo }: RepoPullRequestsProps) { fetchPullRequests() }, [owner, repo]) - useEffect(() => { - const agentModels = AGENT_MODELS[selectedAgent as keyof typeof AGENT_MODELS] - const defaultModel = DEFAULT_MODELS[selectedAgent as keyof typeof DEFAULT_MODELS] - if (agentModels && !agentModels.find((m) => m.value === selectedModel)) { - setSelectedModel(defaultModel) - } - }, [selectedAgent, selectedModel]) - const handleCreateTaskFromPR = (pr: PullRequest) => { setSelectedPR(pr) setShowCreateTaskDialog(true) @@ -218,8 +147,8 @@ export function RepoPullRequests({ owner, repo }: RepoPullRequestsProps) { body: JSON.stringify({ prompt, repoUrl, - selectedAgent, - selectedModel, + selectedAgent: FIXED_TASK_AGENT, + selectedModel: FIXED_TASK_MODEL, installDependencies: installDeps, maxDuration, keepAlive, @@ -432,40 +361,9 @@ export function RepoPullRequests({ owner, repo }: RepoPullRequestsProps) {
-
-
- - -
-
- - -
+
+ This task will run with Codex on{' '} + GPT-5.4.
{/* Task Options */} diff --git a/components/revert-commit-dialog.tsx b/components/revert-commit-dialog.tsx index f4c0a18..c7bf1b7 100644 --- a/components/revert-commit-dialog.tsx +++ b/components/revert-commit-dialog.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect, startTransition } from 'react' +import { useState } from 'react' import { AlertDialog, AlertDialogAction, @@ -14,7 +14,6 @@ import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Checkbox } from '@/components/ui/checkbox' import { Label } from '@/components/ui/label' -import { Claude, Codex, Copilot, Cursor, Gemini, OpenCode } from '@/components/logos' interface Commit { sha: string @@ -50,61 +49,8 @@ interface RevertCommitDialogProps { maxSandboxDuration?: number } -const CODING_AGENTS = [ - { value: 'claude', label: 'Claude', icon: Claude }, - { value: 'codex', label: 'Codex', icon: Codex }, - { value: 'copilot', label: 'Copilot', icon: Copilot }, - { value: 'cursor', label: 'Cursor', icon: Cursor }, - { value: 'gemini', label: 'Gemini', icon: Gemini }, - { value: 'opencode', label: 'opencode', icon: OpenCode }, -] as const - -const AGENT_MODELS = { - claude: [ - { value: 'claude-sonnet-4-5', label: 'Sonnet 4.5' }, - { value: 'anthropic/claude-opus-4.6', label: 'Opus 4.6' }, - { value: 'claude-haiku-4-5', label: 'Haiku 4.5' }, - ], - codex: [{ value: 'gpt-5.4', label: 'GPT-5.4' }], - copilot: [ - { value: 'claude-sonnet-4.5', label: 'Sonnet 4.5' }, - { value: 'claude-sonnet-4', label: 'Sonnet 4' }, - { value: 'claude-haiku-4.5', label: 'Haiku 4.5' }, - { value: 'gpt-5', label: 'GPT-5' }, - ], - cursor: [ - { value: 'auto', label: 'Auto' }, - { value: 'sonnet-4.5', label: 'Sonnet 4.5' }, - { value: 'sonnet-4.5-thinking', label: 'Sonnet 4.5 Thinking' }, - { value: 'gpt-5', label: 'GPT-5' }, - { value: 'gpt-5-codex', label: 'GPT-5 Codex' }, - { value: 'opus-4.1', label: 'Opus 4.1' }, - { value: 'grok', label: 'Grok' }, - ], - gemini: [ - { value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' }, - { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' }, - { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' }, - ], - opencode: [ - { value: 'gpt-5', label: 'GPT-5' }, - { value: 'gpt-5-mini', label: 'GPT-5 Mini' }, - { value: 'gpt-5-nano', label: 'GPT-5 Nano' }, - { value: 'gpt-4.1', label: 'GPT-4.1' }, - { value: 'claude-sonnet-4-5', label: 'Sonnet 4.5' }, - { value: 'claude-opus-4-5', label: 'Opus 4.5' }, - { value: 'claude-haiku-4-5', label: 'Haiku 4.5' }, - ], -} as const - -const DEFAULT_MODELS = { - claude: 'claude-sonnet-4-5', - codex: 'gpt-5.4', - copilot: 'claude-sonnet-4.5', - cursor: 'auto', - gemini: 'gemini-3-pro-preview', - opencode: 'gpt-5', -} as const +const FIXED_TASK_AGENT = 'codex' +const FIXED_TASK_MODEL = 'gpt-5.4' export function RevertCommitDialog({ open, @@ -115,37 +61,19 @@ export function RevertCommitDialog({ onRevert, maxSandboxDuration = 300, }: RevertCommitDialogProps) { - const [selectedAgent, setSelectedAgent] = useState('claude') - const [selectedModel, setSelectedModel] = useState(DEFAULT_MODELS.claude) const [installDependencies, setInstallDependencies] = useState(false) const [maxDuration, setMaxDuration] = useState(300) const [keepAlive, setKeepAlive] = useState(false) const [isReverting, setIsReverting] = useState(false) - // Update model when agent changes - useEffect(() => { - if (selectedAgent) { - const agentModels = AGENT_MODELS[selectedAgent as keyof typeof AGENT_MODELS] - const defaultModel = DEFAULT_MODELS[selectedAgent as keyof typeof DEFAULT_MODELS] - // Check if current model exists for the new agent, otherwise use default - const modelExists = agentModels?.some((m) => m.value === selectedModel) - if (!modelExists) { - // Use startTransition to defer state update - startTransition(() => { - setSelectedModel(defaultModel) - }) - } - } - }, [selectedAgent, selectedModel]) - const handleRevert = () => { if (!commit) return setIsReverting(true) onRevert({ commit, - selectedAgent, - selectedModel, + selectedAgent: FIXED_TASK_AGENT, + selectedModel: FIXED_TASK_MODEL, installDependencies, maxDuration, keepAlive, @@ -170,40 +98,9 @@ export function RevertCommitDialog({
-
-
- - -
-
- - -
+
+ This task will run with Codex on{' '} + GPT-5.4.
{/* Task Options */} diff --git a/components/task-details.tsx b/components/task-details.tsx index a1a4bd5..564cbca 100644 --- a/components/task-details.tsx +++ b/components/task-details.tsx @@ -103,62 +103,8 @@ interface DiffData { language: string } -const CODING_AGENTS = [ - { value: 'claude', label: 'Claude', icon: Claude }, - { value: 'codex', label: 'Codex', icon: Codex }, - { value: 'copilot', label: 'Copilot', icon: Copilot }, - { value: 'cursor', label: 'Cursor', icon: Cursor }, - { value: 'gemini', label: 'Gemini', icon: Gemini }, - { value: 'opencode', label: 'opencode', icon: OpenCode }, -] as const - -const AGENT_MODELS = { - claude: [ - { value: 'claude-sonnet-4-5', label: 'Sonnet 4.5' }, - { value: 'anthropic/claude-opus-4.6', label: 'Opus 4.6' }, - { value: 'claude-haiku-4-5', label: 'Haiku 4.5' }, - ], - codex: [{ value: 'gpt-5.4', label: 'GPT-5.4' }], - copilot: [ - { value: 'claude-sonnet-4.5', label: 'Sonnet 4.5' }, - { value: 'claude-sonnet-4', label: 'Sonnet 4' }, - { value: 'claude-haiku-4.5', label: 'Haiku 4.5' }, - { value: 'gpt-5', label: 'GPT-5' }, - ], - cursor: [ - { value: 'auto', label: 'Auto' }, - { value: 'composer-1', label: 'Composer' }, - { value: 'sonnet-4.5', label: 'Sonnet 4.5' }, - { value: 'sonnet-4.5-thinking', label: 'Sonnet 4.5 Thinking' }, - { value: 'gpt-5', label: 'GPT-5' }, - { value: 'gpt-5-codex', label: 'GPT-5 Codex' }, - { value: 'opus-4.1', label: 'Opus 4.1' }, - { value: 'grok', label: 'Grok' }, - ], - gemini: [ - { value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' }, - { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' }, - { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' }, - ], - opencode: [ - { value: 'gpt-5', label: 'GPT-5' }, - { value: 'gpt-5-mini', label: 'GPT-5 Mini' }, - { value: 'gpt-5-nano', label: 'GPT-5 Nano' }, - { value: 'gpt-4.1', label: 'GPT-4.1' }, - { value: 'claude-sonnet-4-5', label: 'Sonnet 4.5' }, - { value: 'claude-opus-4-5', label: 'Opus 4.5' }, - { value: 'claude-haiku-4-5', label: 'Haiku 4.5' }, - ], -} as const - -const DEFAULT_MODELS = { - claude: 'claude-sonnet-4-5', - codex: 'gpt-5.4', - copilot: 'claude-sonnet-4.5', - cursor: 'auto', - gemini: 'gemini-3-pro-preview', - opencode: 'gpt-5', -} as const +const FIXED_TASK_AGENT = 'codex' +const FIXED_TASK_MODEL = 'gpt-5.4' export function TaskDetails({ task, maxSandboxDuration = 300 }: TaskDetailsProps) { const isDevboxRuntimeTask = task.runtimeProvider === 'devbox' || Boolean(task.runtimeName) @@ -174,10 +120,6 @@ export function TaskDetails({ task, maxSandboxDuration = 300 }: TaskDetailsProps const [showTryAgainDialog, setShowTryAgainDialog] = useState(false) const [isDeleting, setIsDeleting] = useState(false) const [isTryingAgain, setIsTryingAgain] = useState(false) - const [selectedAgent, setSelectedAgent] = useState(task.selectedAgent || 'claude') - const [selectedModel, setSelectedModel] = useState( - task.selectedModel || DEFAULT_MODELS[(task.selectedAgent as keyof typeof DEFAULT_MODELS) || 'claude'], - ) const [tryAgainInstallDeps, setTryAgainInstallDeps] = useState(task.installDependencies || false) const [tryAgainMaxDuration, setTryAgainMaxDuration] = useState(task.maxDuration || maxSandboxDuration) const [tryAgainKeepAlive, setTryAgainKeepAlive] = useState(task.keepAlive || false) @@ -223,23 +165,6 @@ export function TaskDetails({ task, maxSandboxDuration = 300 }: TaskDetailsProps const healthyCountRef = useRef(0) const lastHealthStatusRef = useRef(null) - // Initialize model correctly on mount and when agent changes in Try Again dialog - useEffect(() => { - const agent = selectedAgent as keyof typeof DEFAULT_MODELS - const taskModel = task.selectedModel - - // Check if the task's model exists in the agent's model list - const agentModels = AGENT_MODELS[agent] - const modelExists = agentModels?.some((m) => m.value === taskModel) - - // Use task model if it exists for the agent, otherwise use default - const correctModel = modelExists && taskModel ? taskModel : DEFAULT_MODELS[agent] - - if (correctModel !== selectedModel) { - setSelectedModel(correctModel) - } - }, [selectedAgent, task.selectedModel, selectedModel]) - // File search state const [fileSearchQuery, setFileSearchQuery] = useState('') const [showFileDropdown, setShowFileDropdown] = useState(false) @@ -1131,17 +1056,6 @@ export function TaskDetails({ task, maxSandboxDuration = 300 }: TaskDetailsProps previousStatusRef.current = currentStatus }, [task.status, optimisticStatus]) - // Update model when agent changes - useEffect(() => { - if (selectedAgent) { - const agentModels = AGENT_MODELS[selectedAgent as keyof typeof AGENT_MODELS] - const defaultModel = DEFAULT_MODELS[selectedAgent as keyof typeof DEFAULT_MODELS] - if (defaultModel && agentModels) { - setSelectedModel(defaultModel) - } - } - }, [selectedAgent]) - // Scroll active tab into view when it changes useEffect(() => { const tabKey = `${viewMode}-${activeTabIndex}` @@ -1263,8 +1177,8 @@ export function TaskDetails({ task, maxSandboxDuration = 300 }: TaskDetailsProps body: JSON.stringify({ prompt: task.prompt, repoUrl: task.repoUrl, - selectedAgent, - selectedModel, + selectedAgent: FIXED_TASK_AGENT, + selectedModel: FIXED_TASK_MODEL, installDependencies: tryAgainInstallDeps, maxDuration: tryAgainMaxDuration, keepAlive: tryAgainKeepAlive, @@ -2490,40 +2404,9 @@ export function TaskDetails({ task, maxSandboxDuration = 300 }: TaskDetailsProps Create a new task with the same prompt and repository.
-
-
- - -
-
- - -
+
+ This action always creates a new task with Codex on{' '} + GPT-5.4.
{/* Task Options */} diff --git a/drizzle.config.ts b/drizzle.config.ts index 2e10a69..324f95e 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,10 +1,26 @@ +import { existsSync } from 'node:fs' +import { resolve } from 'node:path' +import { config } from 'dotenv' import { defineConfig } from 'drizzle-kit' +const envLocalPath = resolve(process.cwd(), '.env.local') +const envPath = resolve(process.cwd(), '.env') + +if (existsSync(envLocalPath)) { + config({ path: envLocalPath }) +} else if (existsSync(envPath)) { + config({ path: envPath }) +} + +if (!process.env.POSTGRES_URL) { + throw new Error('POSTGRES_URL is required for Drizzle commands') +} + export default defineConfig({ schema: './lib/db/schema.ts', out: './lib/db/migrations', dialect: 'postgresql', dbCredentials: { - url: process.env.POSTGRES_URL!, + url: process.env.POSTGRES_URL, }, }) diff --git a/lib/atoms/multi-repo.ts b/lib/atoms/multi-repo.ts deleted file mode 100644 index ef4fd99..0000000 --- a/lib/atoms/multi-repo.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { atom } from 'jotai' - -export interface SelectedRepo { - owner: string - repo: string - full_name: string - clone_url: string -} - -// Whether multi-repo mode is enabled -export const multiRepoModeAtom = atom(false) - -// Selected repos in multi-repo mode -export const selectedReposAtom = atom([]) diff --git a/lib/db/migrations/0028_solid_lockjaw.sql b/lib/db/migrations/0028_solid_lockjaw.sql new file mode 100644 index 0000000..d81b1f8 --- /dev/null +++ b/lib/db/migrations/0028_solid_lockjaw.sql @@ -0,0 +1 @@ +ALTER TABLE "tasks" ALTER COLUMN "selected_agent" SET DEFAULT 'codex'; diff --git a/lib/db/migrations/meta/_journal.json b/lib/db/migrations/meta/_journal.json index 6badf8a..40d0b52 100644 --- a/lib/db/migrations/meta/_journal.json +++ b/lib/db/migrations/meta/_journal.json @@ -197,6 +197,13 @@ "when": 1776766917758, "tag": "0027_windy_pet_avengers", "breakpoints": true + }, + { + "idx": 28, + "version": "7", + "when": 1776910000000, + "tag": "0028_solid_lockjaw", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 8f76e80..61424c2 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -81,7 +81,7 @@ export const tasks = pgTable('tasks', { prompt: text('prompt').notNull(), title: text('title'), repoUrl: text('repo_url'), - selectedAgent: text('selected_agent').default('claude'), + selectedAgent: text('selected_agent').default('codex'), selectedModel: text('selected_model'), installDependencies: boolean('install_dependencies').default(false), maxDuration: integer('max_duration').default(parseInt(process.env.MAX_SANDBOX_DURATION || '300', 10)), @@ -137,7 +137,7 @@ export const insertTaskSchema = z.object({ prompt: z.string().min(1, 'Prompt is required'), title: z.string().optional(), repoUrl: z.string().url('Must be a valid URL').optional(), - selectedAgent: z.enum(['claude', 'codex', 'copilot', 'cursor', 'gemini', 'opencode']).default('claude'), + selectedAgent: z.enum(['claude', 'codex', 'copilot', 'cursor', 'gemini', 'opencode']).default('codex'), selectedModel: z.string().optional(), installDependencies: z.boolean().default(false), maxDuration: z.number().default(parseInt(process.env.MAX_SANDBOX_DURATION || '300', 10)), diff --git a/reference/architecture.md b/reference/architecture.md new file mode 100644 index 0000000..82ca5a8 --- /dev/null +++ b/reference/architecture.md @@ -0,0 +1,244 @@ +# Architecture + +This document explains how Analyze and Ship to Sealos is organized, how a task moves through the system, and where to look when changing core behavior. + +## System Overview + +The app is a Next.js App Router application with three main responsibilities: + +1. Present a Sealos-oriented task UI for authenticated users. +2. Persist task, auth, and connector state in Postgres through Drizzle ORM. +3. Orchestrate a fixed `codex` + `gpt-5.4` execution path that runs inside a Devbox runtime and is driven through the Codex Gateway. + +At a high level: + +- The home page is the entry point for choosing a repository and starting a task. +- Task API routes create and manage task state. +- Devbox code provisions or resumes the runtime that hosts the task workspace. +- Codex Gateway code opens sessions, sends turns, proxies streams, and finalizes task completion. +- Task pages surface the state of that work back to the user. + +## Main User Surfaces + +### Home page + +- Route: `app/page.tsx` +- Main UI: `components/sealos-home-page-content.tsx` +- Input form: `components/task-form.tsx` + +This flow is optimized for a single deployment-oriented command surface: + +- the user signs in +- chooses a GitHub repository +- submits a prompt +- is redirected into the task workspace + +### Task workspace + +- Route: `app/tasks/[taskId]/page.tsx` +- Main UI: `components/sealos-task-page-client.tsx` +- Chat UI: `components/task-chat.tsx` + +The task workspace is the main operational screen. It shows: + +- current task title or prompt +- chat and follow-up turns +- task actions +- task status as it changes over time + +Other supporting task UI lives in components such as `components/task-details.tsx`, `components/file-browser.tsx`, and related dialogs. + +### Repository views + +- Layout route: `app/repos/[owner]/[repo]/layout.tsx` +- Shared UI: `components/repo-layout.tsx` + +These pages expose repository context outside the task flow: + +- commits +- issues +- pull requests + +## Execution Flow + +### 1. Task submission + +The client submits a task through `POST /api/tasks` in `app/api/tasks/route.ts`. + +That route: + +- validates input against the Drizzle-backed Zod schema +- forces the execution path to `codex` +- forces the model to `gpt-5.4` +- inserts the task row +- starts the first task turn +- kicks off asynchronous title and branch-name generation + +This keeps the request responsive while still recording task metadata early. + +### 2. Runtime preparation + +Before a turn is sent to Codex Gateway, the app ensures a Devbox runtime exists for the task. + +Primary code: + +- `lib/devbox/runtime.ts` +- `lib/devbox/client.ts` +- `lib/devbox/config.ts` + +The runtime layer is responsible for: + +- creating or reusing a runtime +- restoring paused runtimes +- refreshing the runtime lease +- preparing the workspace for the task repository +- resolving gateway connection details from runtime metadata + +### 3. Gateway turn execution + +Primary code: + +- `lib/codex-gateway/chat-v2-service.ts` +- `lib/codex-gateway/runner.ts` +- `lib/codex-gateway/session.ts` +- `lib/codex-gateway/completion.ts` + +The gateway layer: + +- ensures a gateway session exists +- prepends Sealos deployment context on the first turn +- sends the user prompt to Codex Gateway +- records turn checkpoints and message events +- waits for completion or terminal failure +- updates task state in the database + +### 4. Task state projection + +Task state is persisted in the `tasks` table and related message or event tables in `lib/db/schema.ts`. + +The frontend reads from task APIs and hooks such as: + +- `lib/hooks/use-task.ts` +- `lib/hooks/use-task-agent-chat-v2.ts` + +This keeps the task workspace live while long-running runtime and gateway work is in progress. + +## Core Data Model + +The most important persisted entities are: + +- `users`: signed-in users and their primary auth provider state +- `tasks`: task prompt, runtime state, gateway state, PR metadata, logs, and timestamps +- `connectors`: user-level connector definitions + +The `tasks` table acts as the operational center of the app. It stores: + +- prompt and title +- selected agent and selected model +- runtime provider, runtime name, and runtime namespace +- gateway URL and gateway session identifiers +- task status and progress +- PR and preview metadata +- timestamps for lifecycle checkpoints + +## Runtime and Gateway Boundary + +The app intentionally separates runtime concerns from gateway concerns: + +- Devbox owns the execution environment and workspace lifecycle. +- Codex Gateway owns session and turn orchestration. + +The boundary is joined by runtime-derived gateway metadata such as: + +- gateway URL +- gateway auth token +- runtime namespace + +This separation makes it easier to evolve runtime handling and prompt/session handling independently. + +## Routing and Module Boundaries + +### App Router + +Use `app/` for: + +- pages +- layouts +- route handlers +- metadata generation + +### Components + +Use `components/` for: + +- page-level client UI +- task workspace UI +- dialogs and controls +- reusable primitives and `components/ui/` + +### Library code + +Use `lib/` for: + +- database access +- runtime orchestration +- gateway orchestration +- auth and session logic +- utilities and state helpers + +## Logging and Task Flow Messages + +The codebase distinguishes between: + +- task-facing logs persisted on the task +- server-side console logging for diagnostics + +For task-facing flow logs, use the helpers in: + +- `lib/utils/task-logger.ts` +- `lib/utils/task-flow-logs.ts` + +This is important because the project has explicit restrictions on dynamic values in logs. + +## Where To Start When Changing Something + +### Change the submission flow + +Start with: + +- `components/task-form.tsx` +- `components/sealos-home-page-content.tsx` +- `app/api/tasks/route.ts` + +### Change runtime behavior + +Start with: + +- `lib/devbox/runtime.ts` +- `lib/devbox/config.ts` +- `lib/sealos/config.ts` + +### Change Codex execution behavior + +Start with: + +- `lib/codex-gateway/chat-v2-service.ts` +- `lib/codex-gateway/runner.ts` +- `lib/sealos-deploy-context.ts` + +### Change task UI + +Start with: + +- `components/sealos-task-page-client.tsx` +- `components/task-chat.tsx` +- `components/task-details.tsx` + +### Change repository pages + +Start with: + +- `components/repo-layout.tsx` +- `components/repo-commits.tsx` +- `components/repo-issues.tsx` +- `components/repo-pull-requests.tsx` diff --git a/reference/configuration.md b/reference/configuration.md new file mode 100644 index 0000000..6deb65a --- /dev/null +++ b/reference/configuration.md @@ -0,0 +1,175 @@ +# Configuration + +This document collects the configuration and operational details that are useful when setting up, running, or extending the project locally. + +## Required Environment Variables + +### Core app infrastructure + +These values are required for the app to boot and run its main task flow: + +- `POSTGRES_URL`: Postgres connection string used by the app and Drizzle commands +- `SEALOS_HOST`: Sealos region host, for example `staging-usw-1.sealos.io` +- `DEVBOX_TOKEN`: static Devbox API token +- `JWE_SECRET`: secret for session encryption +- `ENCRYPTION_KEY`: symmetric key used to encrypt stored tokens and user API keys +- `NEXT_PUBLIC_AUTH_PROVIDERS`: should include `github` for the current primary flow +- `GITHUB_CLIENT_ID`: GitHub OAuth client ID +- `GITHUB_CLIENT_SECRET`: GitHub OAuth client secret + +### Codex execution + +- `AI_GATEWAY_API_KEY`: required for the current Codex execution path unless users are expected to supply their own key through the UI + +## Optional Environment Variables + +- `APP_BASE_URL`: explicit public base URL for OAuth callbacks in self-hosted deployments +- `NPM_TOKEN`: used when runtimes need to install private npm dependencies +- `MAX_SANDBOX_DURATION`: default runtime lifetime in minutes +- `MAX_MESSAGES_PER_DAY`: per-user daily limit for tasks and follow-up turns +- `DEVBOX_NAMESPACE`: override the default Devbox namespace +- `DEVBOX_RUNTIME_IMAGE`: override the runtime image +- `DEVBOX_ARCHIVE_AFTER_PAUSE_TIME`: archive timing after runtime pause +- `DEVBOX_JWT_SIGNING_KEY`: required when `DEVBOX_TOKEN` is not set and JWT auth is used instead +- `DEVBOX_JWT_TTL_SECONDS`: token lifetime for JWT-based Devbox auth +- `CODEX_GATEWAY_SESSION_TTL_MS`: session TTL for Codex Gateway + +## Derived Sealos Values + +The app derives several URLs from `SEALOS_HOST` in `lib/sealos/config.ts`. + +For a host such as `staging-usw-1.sealos.io`, the app derives: + +- region: `staging-usw-1` +- region URL: `https://staging-usw-1.sealos.io` +- template API: `https://template.staging-usw-1.sealos.io/api/v2alpha/templates/raw` +- Devbox base URL: `https://devbox-server.staging-usw-1.sealos.io` + +## Authentication Setup + +### GitHub OAuth + +The current primary sign-in flow is GitHub-based. + +Required OAuth scopes are defined in `lib/auth/oauth.ts`: + +- `repo` +- `read:user` +- `user:email` +- `read:packages` +- `write:packages` + +The GitHub callback route is: + +- `/api/auth/github/callback` + +For local development, a typical OAuth app setup uses: + +- homepage URL: `http://localhost:3000` +- callback URL: `http://localhost:3000/api/auth/github/callback` + +### App base URL behavior + +If `APP_BASE_URL` is set, it is used for callback resolution. Otherwise, the app falls back to forwarded headers and finally to `req.nextUrl.origin`. + +## Devbox Authentication Modes + +The app supports two ways to authenticate to Devbox: + +### Static token + +If `DEVBOX_TOKEN` is present, it is used directly. + +### JWT signing mode + +If `DEVBOX_TOKEN` is absent, the app expects: + +- `DEVBOX_JWT_SIGNING_KEY` +- optionally `DEVBOX_JWT_TTL_SECONDS` + +In this mode the app signs short-lived JWTs scoped to the configured namespace. + +## Runtime Behavior + +The main task flow runs inside a Devbox runtime. + +Key behaviors: + +- runtimes can be created, resumed, and refreshed +- runtime namespace defaults to `ns-test` unless overridden +- runtime lifetime is bounded by `MAX_SANDBOX_DURATION` or a task-specific duration +- if task retention is enabled, the runtime remains available for follow-up work until timeout + +The code that governs this behavior lives mainly in: + +- `lib/devbox/runtime.ts` +- `lib/devbox/config.ts` +- `lib/sealos/config.ts` + +## Codex Gateway Configuration + +The project is intentionally pinned to a fixed execution path: + +- agent: `codex` +- model: `gpt-5.4` + +Relevant code: + +- `lib/codex/defaults.ts` +- `lib/codex-gateway/config.ts` +- `app/api/tasks/route.ts` + +Gateway URL and auth token are primarily resolved from Devbox runtime metadata. Existing stored task values are used as fallback when necessary. + +## Database and Migrations + +The project uses Drizzle ORM and checked-in SQL migrations under: + +- `lib/db/migrations/` + +Drizzle configuration lives in: + +- `drizzle.config.ts` + +Important behavior: + +- `.env.local` is loaded first +- `.env` is used as fallback +- Drizzle commands fail fast if `POSTGRES_URL` is missing + +Typical commands: + +```bash +pnpm db:generate +pnpm db:migrate +pnpm db:studio +``` + +## Local Development Checklist + +1. Create `.env.local` with required values. +2. Run `pnpm install`. +3. Run `pnpm db:migrate`. +4. Run `pnpm dev`. +5. Sign in with GitHub. +6. Choose a repository and create a task. + +## Operational Notes + +### User-scoped API keys + +Users can provide their own API keys through the application. Those values can override global configuration for supported flows. + +### Connector encryption + +Connector secrets and user API keys depend on `ENCRYPTION_KEY`. If that key is missing, encrypted storage paths will not work correctly. + +### Logging restrictions + +The repository has strict logging rules: + +- default to static log messages +- only allow dynamic runtime identifiers through the task-flow logging utility +- avoid sensitive values in logs and user-facing errors + +Read `AGENTS.md` before changing logging, auth, or task execution code.