diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index bedf31ca..c4d09171 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "server" version = "0.0.73" -edition = "2021" +edition = "2024" default-run = "server" [lints.clippy] diff --git a/crates/server/src/bin/generate_types.rs b/crates/server/src/bin/generate_types.rs index f8479497..4a554b6d 100644 --- a/crates/server/src/bin/generate_types.rs +++ b/crates/server/src/bin/generate_types.rs @@ -129,9 +129,6 @@ fn main() { println!("Generating TypeScript types…"); - // 2. Let ts-rs write its per-type files here (handy for debugging) - env::set_var("TS_RS_EXPORT_DIR", shared_path.to_str().unwrap()); - let generated = generate_types_content(); let types_path = shared_path.join("types.ts"); @@ -142,7 +139,9 @@ fn main() { println!("✅ shared/types.ts is up to date."); std::process::exit(0); } else { - eprintln!("❌ shared/types.ts is not up to date. Please run 'npm run generate-types' and commit the changes."); + eprintln!( + "❌ shared/types.ts is not up to date. Please run 'npm run generate-types' and commit the changes." + ); std::process::exit(1); } } else { diff --git a/crates/server/src/bin/mcp_task_server.rs b/crates/server/src/bin/mcp_task_server.rs index c229d94c..987f7ca1 100644 --- a/crates/server/src/bin/mcp_task_server.rs +++ b/crates/server/src/bin/mcp_task_server.rs @@ -1,9 +1,9 @@ use std::str::FromStr; -use rmcp::{transport::stdio, ServiceExt}; +use rmcp::{ServiceExt, transport::stdio}; use server::mcp::task_server::TaskServer; -use sqlx::{sqlite::SqliteConnectOptions, SqlitePool}; -use tracing_subscriber::{prelude::*, EnvFilter}; +use sqlx::{SqlitePool, sqlite::SqliteConnectOptions}; +use tracing_subscriber::{EnvFilter, prelude::*}; use utils::{assets::asset_dir, sentry::sentry_layer}; fn main() -> anyhow::Result<()> { @@ -12,11 +12,14 @@ fn main() -> anyhow::Result<()> { } else { "production" }; - let _guard = sentry::init(("https://1065a1d276a581316999a07d5dffee26@o4509603705192449.ingest.de.sentry.io/4509605576441937", sentry::ClientOptions { - release: sentry::release_name!(), - environment: Some(environment.into()), - ..Default::default() - })); + let _guard = sentry::init(( + "https://1065a1d276a581316999a07d5dffee26@o4509603705192449.ingest.de.sentry.io/4509605576441937", + sentry::ClientOptions { + release: sentry::release_name!(), + environment: Some(environment.into()), + ..Default::default() + }, + )); sentry::configure_scope(|scope| { scope.set_tag("source", "mcp"); }); diff --git a/crates/server/src/error.rs b/crates/server/src/error.rs index c8807e2b..e9edb16a 100644 --- a/crates/server/src/error.rs +++ b/crates/server/src/error.rs @@ -1,8 +1,8 @@ use axum::{ + Json, extract::multipart::MultipartError, http::StatusCode, response::{IntoResponse, Response}, - Json, }; use db::models::{project::ProjectError, task_attempt::TaskAttemptError}; use deployment::DeploymentError; diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index d257a886..c9b817c5 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -1,10 +1,10 @@ use anyhow::{self, Error as AnyhowError}; use deployment::{Deployment, DeploymentError}; -use server::{routes, DeploymentImpl}; +use server::{DeploymentImpl, routes}; use sqlx::Error as SqlxError; use strip_ansi_escapes::strip; use thiserror::Error; -use tracing_subscriber::{prelude::*, EnvFilter}; +use tracing_subscriber::{EnvFilter, prelude::*}; use utils::{ assets::asset_dir, browser::open_browser, port_file::write_port_file, sentry::sentry_layer, }; @@ -79,7 +79,11 @@ async fn main() -> Result<(), VibeKanbanError> { if !cfg!(debug_assertions) { tracing::info!("Opening browser..."); if let Err(e) = open_browser(&format!("http://127.0.0.1:{actual_port}")).await { - tracing::warn!("Failed to open browser automatically: {}. Please open http://127.0.0.1:{} manually.", e, actual_port); + tracing::warn!( + "Failed to open browser automatically: {}. Please open http://127.0.0.1:{} manually.", + e, + actual_port + ); } } diff --git a/crates/server/src/mcp/task_server.rs b/crates/server/src/mcp/task_server.rs index 3ee59d9e..d69a865a 100644 --- a/crates/server/src/mcp/task_server.rs +++ b/crates/server/src/mcp/task_server.rs @@ -5,11 +5,12 @@ use db::models::{ task::{CreateTask, Task, TaskStatus}, }; use rmcp::{ + ErrorData, ServerHandler, handler::server::tool::{Parameters, ToolRouter}, model::{ CallToolResult, Content, Implementation, ProtocolVersion, ServerCapabilities, ServerInfo, }, - schemars, tool, tool_handler, tool_router, ErrorData, ServerHandler, + schemars, tool, tool_handler, tool_router, }; use serde::{Deserialize, Serialize}; use serde_json; diff --git a/crates/server/src/routes/auth.rs b/crates/server/src/routes/auth.rs index 77e028cc..77efcef2 100644 --- a/crates/server/src/routes/auth.rs +++ b/crates/server/src/routes/auth.rs @@ -1,10 +1,10 @@ use axum::{ + Router, extract::{Request, State}, http::StatusCode, - middleware::{from_fn_with_state, Next}, + middleware::{Next, from_fn_with_state}, response::{Json as ResponseJson, Response}, routing::{get, post}, - Router, }; use deployment::Deployment; use octocrab::auth::Continue; @@ -16,7 +16,7 @@ use services::services::{ }; use utils::response::ApiResponse; -use crate::{error::ApiError, DeploymentImpl}; +use crate::{DeploymentImpl, error::ApiError}; pub fn router(deployment: &DeploymentImpl) -> Router { Router::new() diff --git a/crates/server/src/routes/config.rs b/crates/server/src/routes/config.rs index cc814f9e..4504cb52 100644 --- a/crates/server/src/routes/config.rs +++ b/crates/server/src/routes/config.rs @@ -1,27 +1,27 @@ use std::collections::HashMap; use axum::{ + Json, Router, body::Body, extract::{Path, Query, State}, http, response::{Json as ResponseJson, Response}, routing::{get, put}, - Json, Router, }; use deployment::{Deployment, DeploymentError}; use executors::{ executors::{BaseCodingAgent, StandardCodingAgentExecutor}, - mcp_config::{read_agent_config, write_agent_config, McpConfig}, + mcp_config::{McpConfig, read_agent_config, write_agent_config}, profile::{ExecutorConfigs, ExecutorProfileId}, }; use serde::{Deserialize, Serialize}; use serde_json::Value; -use services::services::config::{save_config_to_file, Config, ConfigError, SoundFile}; +use services::services::config::{Config, ConfigError, SoundFile, save_config_to_file}; use tokio::fs; use ts_rs::TS; use utils::{assets::config_path, response::ApiResponse}; -use crate::{error::ApiError, DeploymentImpl}; +use crate::{DeploymentImpl, error::ApiError}; pub fn router() -> Router { Router::new() @@ -190,7 +190,7 @@ async fn update_mcp_servers( None => { return Ok(ResponseJson(ApiResponse::error( "Could not determine config file path", - ))) + ))); } }; diff --git a/crates/server/src/routes/containers.rs b/crates/server/src/routes/containers.rs index 89f8b157..a02ad22f 100644 --- a/crates/server/src/routes/containers.rs +++ b/crates/server/src/routes/containers.rs @@ -1,8 +1,8 @@ use axum::{ + Router, extract::{Query, State}, response::Json as ResponseJson, routing::get, - Router, }; use db::models::task_attempt::TaskAttempt; use deployment::Deployment; @@ -11,7 +11,7 @@ use ts_rs::TS; use utils::response::ApiResponse; use uuid::Uuid; -use crate::{error::ApiError, DeploymentImpl}; +use crate::{DeploymentImpl, error::ApiError}; #[derive(Debug, Serialize, TS)] pub struct ContainerInfo { diff --git a/crates/server/src/routes/events.rs b/crates/server/src/routes/events.rs index 6e74fc64..3be68d9f 100644 --- a/crates/server/src/routes/events.rs +++ b/crates/server/src/routes/events.rs @@ -1,11 +1,11 @@ use axum::{ + BoxError, Router, extract::State, response::{ - sse::{Event, KeepAlive}, Sse, + sse::{Event, KeepAlive}, }, routing::get, - BoxError, Router, }; use deployment::Deployment; use futures_util::TryStreamExt; diff --git a/crates/server/src/routes/execution_processes.rs b/crates/server/src/routes/execution_processes.rs index a5a91812..81da38b4 100644 --- a/crates/server/src/routes/execution_processes.rs +++ b/crates/server/src/routes/execution_processes.rs @@ -1,12 +1,12 @@ use axum::{ + BoxError, Extension, Router, extract::{Path, Query, State}, middleware::from_fn_with_state, response::{ - sse::{Event, KeepAlive}, Json as ResponseJson, Sse, + sse::{Event, KeepAlive}, }, routing::{get, post}, - BoxError, Extension, Router, }; use db::models::execution_process::ExecutionProcess; use deployment::Deployment; @@ -16,7 +16,7 @@ use services::services::container::ContainerService; use utils::response::ApiResponse; use uuid::Uuid; -use crate::{error::ApiError, middleware::load_execution_process_middleware, DeploymentImpl}; +use crate::{DeploymentImpl, error::ApiError, middleware::load_execution_process_middleware}; #[derive(Debug, Deserialize)] pub struct ExecutionProcessQuery { diff --git a/crates/server/src/routes/filesystem.rs b/crates/server/src/routes/filesystem.rs index 572191c0..41ef5090 100644 --- a/crates/server/src/routes/filesystem.rs +++ b/crates/server/src/routes/filesystem.rs @@ -1,15 +1,15 @@ use axum::{ + Router, extract::{Query, State}, response::Json as ResponseJson, routing::get, - Router, }; use deployment::Deployment; use serde::Deserialize; use services::services::filesystem::{DirectoryEntry, DirectoryListResponse, FilesystemError}; use utils::response::ApiResponse; -use crate::{error::ApiError, DeploymentImpl}; +use crate::{DeploymentImpl, error::ApiError}; #[derive(Debug, Deserialize)] pub struct ListDirectoryQuery { diff --git a/crates/server/src/routes/frontend.rs b/crates/server/src/routes/frontend.rs index a5f019cc..4aedff0c 100644 --- a/crates/server/src/routes/frontend.rs +++ b/crates/server/src/routes/frontend.rs @@ -3,7 +3,7 @@ use axum::{ http::HeaderValue, response::{IntoResponse, Response}, }; -use reqwest::{header, StatusCode}; +use reqwest::{StatusCode, header}; use rust_embed::RustEmbed; #[derive(RustEmbed)] @@ -19,7 +19,7 @@ pub async fn serve_frontend_root() -> impl IntoResponse { serve_file("index.html").await } -async fn serve_file(path: &str) -> impl IntoResponse { +async fn serve_file(path: &str) -> impl IntoResponse + use<> { let file = Assets::get(path); match file { diff --git a/crates/server/src/routes/images.rs b/crates/server/src/routes/images.rs index 1e8fa7ec..af40a729 100644 --- a/crates/server/src/routes/images.rs +++ b/crates/server/src/routes/images.rs @@ -1,10 +1,10 @@ use axum::{ + Router, body::Body, extract::{DefaultBodyLimit, Multipart, Path, State}, - http::{header, StatusCode}, + http::{StatusCode, header}, response::{Json as ResponseJson, Response}, routing::{delete, get, post}, - Router, }; use chrono::{DateTime, Utc}; use db::models::image::Image; @@ -17,7 +17,7 @@ use ts_rs::TS; use utils::response::ApiResponse; use uuid::Uuid; -use crate::{error::ApiError, DeploymentImpl}; +use crate::{DeploymentImpl, error::ApiError}; #[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct ImageResponse { diff --git a/crates/server/src/routes/mod.rs b/crates/server/src/routes/mod.rs index f60a685c..cc23fb50 100644 --- a/crates/server/src/routes/mod.rs +++ b/crates/server/src/routes/mod.rs @@ -1,6 +1,6 @@ use axum::{ - routing::{get, IntoMakeService}, Router, + routing::{IntoMakeService, get}, }; use crate::DeploymentImpl; diff --git a/crates/server/src/routes/projects.rs b/crates/server/src/routes/projects.rs index 7252ae6a..d0211eff 100644 --- a/crates/server/src/routes/projects.rs +++ b/crates/server/src/routes/projects.rs @@ -1,12 +1,12 @@ use std::{collections::HashMap, path::Path}; use axum::{ + Extension, Json, Router, extract::{Query, State}, http::StatusCode, middleware::from_fn_with_state, response::Json as ResponseJson, routing::{get, post}, - Extension, Json, Router, }; use db::models::project::{ CreateProject, Project, ProjectError, SearchMatchType, SearchResult, UpdateProject, @@ -14,10 +14,10 @@ use db::models::project::{ use deployment::Deployment; use ignore::WalkBuilder; use services::services::{file_ranker::FileRanker, git::GitBranch}; -use utils::response::ApiResponse; +use utils::{path::expand_tilde, response::ApiResponse}; use uuid::Uuid; -use crate::{error::ApiError, middleware::load_project_middleware, DeploymentImpl}; +use crate::{DeploymentImpl, error::ApiError, middleware::load_project_middleware}; pub async fn get_projects( State(deployment): State, @@ -45,11 +45,24 @@ pub async fn create_project( Json(payload): Json, ) -> Result>, ApiError> { let id = Uuid::new_v4(); + let CreateProject { + name, + git_repo_path, + setup_script, + dev_script, + cleanup_script, + copy_files, + use_existing_repo, + } = payload; + tracing::debug!("Creating project '{}'", name); - tracing::debug!("Creating project '{}'", payload.name); - + // Validate and setup git repository + // Expand tilde in git repo path if present + let path = expand_tilde(&git_repo_path); // Check if git repo path is already used by another project - match Project::find_by_git_repo_path(&deployment.db().pool, &payload.git_repo_path).await { + match Project::find_by_git_repo_path(&deployment.db().pool, path.to_string_lossy().as_ref()) + .await + { Ok(Some(_)) => { return Ok(ResponseJson(ApiResponse::error( "A project with this git repository path already exists", @@ -63,10 +76,7 @@ pub async fn create_project( } } - // Validate and setup git repository - let path = std::path::Path::new(&payload.git_repo_path); - - if payload.use_existing_repo { + if use_existing_repo { // For existing repos, validate that the path exists and is a git repository if !path.exists() { return Ok(ResponseJson(ApiResponse::error( @@ -87,7 +97,7 @@ pub async fn create_project( } // Ensure existing repo has a main branch if it's empty - if let Err(e) = deployment.git().ensure_main_branch_exists(path) { + if let Err(e) = deployment.git().ensure_main_branch_exists(&path) { tracing::error!("Failed to ensure main branch exists: {}", e); return Ok(ResponseJson(ApiResponse::error(&format!( "Failed to ensure main branch exists: {}", @@ -99,7 +109,7 @@ pub async fn create_project( // Create directory if it doesn't exist if !path.exists() { - if let Err(e) = std::fs::create_dir_all(path) { + if let Err(e) = std::fs::create_dir_all(&path) { tracing::error!("Failed to create directory: {}", e); return Ok(ResponseJson(ApiResponse::error(&format!( "Failed to create directory: {}", @@ -110,7 +120,7 @@ pub async fn create_project( // Check if it's already a git repo, if not initialize it if !path.join(".git").exists() { - if let Err(e) = deployment.git().initialize_repo_with_main_branch(path) { + if let Err(e) = deployment.git().initialize_repo_with_main_branch(&path) { tracing::error!("Failed to initialize git repository: {}", e); return Ok(ResponseJson(ApiResponse::error(&format!( "Failed to initialize git repository: {}", @@ -120,7 +130,21 @@ pub async fn create_project( } } - match Project::create(&deployment.db().pool, &payload, id).await { + match Project::create( + &deployment.db().pool, + &CreateProject { + name, + git_repo_path: path.to_string_lossy().to_string(), + use_existing_repo, + setup_script, + dev_script, + cleanup_script, + copy_files, + }, + id, + ) + .await + { Ok(project) => { // Track project creation event deployment @@ -128,9 +152,9 @@ pub async fn create_project( "project_created", serde_json::json!({ "project_id": project.id.to_string(), - "use_existing_repo": payload.use_existing_repo, - "has_setup_script": payload.setup_script.is_some(), - "has_dev_script": payload.dev_script.is_some(), + "use_existing_repo": use_existing_repo, + "has_setup_script": project.setup_script.is_some(), + "has_dev_script": project.dev_script.is_some(), }), ) .await; @@ -146,32 +170,6 @@ pub async fn update_project( State(deployment): State, Json(payload): Json, ) -> Result>, StatusCode> { - // If git_repo_path is being changed, check if the new path is already used by another project - if let Some(new_git_repo_path) = &payload.git_repo_path { - if new_git_repo_path != &existing_project.git_repo_path.to_string_lossy() { - match Project::find_by_git_repo_path_excluding_id( - &deployment.db().pool, - new_git_repo_path, - existing_project.id, - ) - .await - { - Ok(Some(_)) => { - return Ok(ResponseJson(ApiResponse::error( - "A project with this git repository path already exists", - ))); - } - Ok(None) => { - // Path is available, continue - } - Err(e) => { - tracing::error!("Failed to check for existing git repo path: {}", e); - return Err(StatusCode::INTERNAL_SERVER_ERROR); - } - } - } - } - // Destructure payload to handle field updates. // This allows us to treat `None` from the payload as an explicit `null` to clear a field, // as the frontend currently sends all fields on update. @@ -183,16 +181,37 @@ pub async fn update_project( cleanup_script, copy_files, } = payload; - - let name = name.unwrap_or(existing_project.name); - let git_repo_path = - git_repo_path.unwrap_or(existing_project.git_repo_path.to_string_lossy().to_string()); + // If git_repo_path is being changed, check if the new path is already used by another project + let git_repo_path = if let Some(new_git_repo_path) = git_repo_path.map(|s| expand_tilde(&s)) + && new_git_repo_path != existing_project.git_repo_path + { + match Project::find_by_git_repo_path_excluding_id( + &deployment.db().pool, + new_git_repo_path.to_string_lossy().as_ref(), + existing_project.id, + ) + .await + { + Ok(Some(_)) => { + return Ok(ResponseJson(ApiResponse::error( + "A project with this git repository path already exists", + ))); + } + Ok(None) => new_git_repo_path, + Err(e) => { + tracing::error!("Failed to check for existing git repo path: {}", e); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + } + } else { + existing_project.git_repo_path + }; match Project::update( &deployment.db().pool, existing_project.id, - name, - git_repo_path, + name.unwrap_or(existing_project.name), + git_repo_path.to_string_lossy().to_string(), setup_script, dev_script, cleanup_script, diff --git a/crates/server/src/routes/task_attempts.rs b/crates/server/src/routes/task_attempts.rs index 04258407..b012b90d 100644 --- a/crates/server/src/routes/task_attempts.rs +++ b/crates/server/src/routes/task_attempts.rs @@ -1,15 +1,15 @@ use std::path::PathBuf; use axum::{ + BoxError, Extension, Json, Router, extract::{Query, State}, http::StatusCode, middleware::from_fn_with_state, response::{ - sse::{Event, KeepAlive}, Json as ResponseJson, Sse, + sse::{Event, KeepAlive}, }, routing::{get, post}, - BoxError, Extension, Json, Router, }; use db::models::{ execution_process::{ExecutionProcess, ExecutionProcessRunReason}, @@ -22,9 +22,9 @@ use db::models::{ use deployment::Deployment; use executors::{ actions::{ + ExecutorAction, ExecutorActionType, coding_agent_follow_up::CodingAgentFollowUpRequest, script::{ScriptContext, ScriptRequest, ScriptRequestLanguage}, - ExecutorAction, ExecutorActionType, }, profile::ExecutorProfileId, }; @@ -41,7 +41,7 @@ use ts_rs::TS; use utils::response::ApiResponse; use uuid::Uuid; -use crate::{error::ApiError, middleware::load_task_attempt_middleware, DeploymentImpl}; +use crate::{DeploymentImpl, error::ApiError, middleware::load_task_attempt_middleware}; #[derive(Debug, Deserialize, Serialize, TS)] pub struct RebaseTaskAttemptRequest { diff --git a/crates/server/src/routes/task_templates.rs b/crates/server/src/routes/task_templates.rs index 9a49c425..fade0b48 100644 --- a/crates/server/src/routes/task_templates.rs +++ b/crates/server/src/routes/task_templates.rs @@ -1,9 +1,9 @@ use axum::{ + Extension, Json, Router, extract::{Query, State}, middleware::from_fn_with_state, response::Json as ResponseJson, routing::get, - Extension, Json, Router, }; use db::models::task_template::{CreateTaskTemplate, TaskTemplate, UpdateTaskTemplate}; use deployment::Deployment; @@ -12,7 +12,7 @@ use sqlx::Error as SqlxError; use utils::response::ApiResponse; use uuid::Uuid; -use crate::{error::ApiError, middleware::load_task_template_middleware, DeploymentImpl}; +use crate::{DeploymentImpl, error::ApiError, middleware::load_task_template_middleware}; #[derive(Debug, Deserialize)] pub struct TaskTemplateQuery { diff --git a/crates/server/src/routes/tasks.rs b/crates/server/src/routes/tasks.rs index c7656409..27e0521a 100644 --- a/crates/server/src/routes/tasks.rs +++ b/crates/server/src/routes/tasks.rs @@ -1,9 +1,9 @@ use axum::{ + BoxError, Extension, Json, Router, extract::{Query, State}, middleware::from_fn_with_state, - response::{sse::KeepAlive, Json as ResponseJson, Sse}, + response::{Json as ResponseJson, Sse, sse::KeepAlive}, routing::{get, post}, - BoxError, Extension, Json, Router, }; use db::models::{ image::TaskImage, @@ -19,7 +19,7 @@ use sqlx::Error as SqlxError; use utils::response::ApiResponse; use uuid::Uuid; -use crate::{error::ApiError, middleware::load_task_middleware, DeploymentImpl}; +use crate::{DeploymentImpl, error::ApiError, middleware::load_task_middleware}; #[derive(Debug, Deserialize)] pub struct TaskQuery { diff --git a/frontend/src/components/projects/project-form-fields.tsx b/frontend/src/components/projects/project-form-fields.tsx index 0cc25045..843e794c 100644 --- a/frontend/src/components/projects/project-form-fields.tsx +++ b/frontend/src/components/projects/project-form-fields.tsx @@ -1,14 +1,26 @@ +import { useState, useEffect } from 'react'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; -import { AlertCircle, Folder } from 'lucide-react'; +import { + AlertCircle, + Folder, + Search, + FolderGit, + FolderPlus, + ArrowLeft, +} from 'lucide-react'; import { createScriptPlaceholderStrategy, ScriptPlaceholderContext, } from '@/utils/script-placeholders'; import { useUserSystem } from '@/components/config-provider'; import { CopyFilesField } from './copy-files-field'; +// Removed collapsible sections for simplicity; show fields always in edit mode +import { fileSystemApi } from '@/lib/api'; +import { DirectoryEntry } from 'shared/types'; +import { generateProjectNameFromPath } from '@/utils/string'; interface ProjectFormFieldsProps { isEditing: boolean; @@ -19,7 +31,6 @@ interface ProjectFormFieldsProps { setShowFolderPicker: (show: boolean) => void; parentPath: string; setParentPath: (path: string) => void; - folderName: string; setFolderName: (name: string) => void; setName: (name: string) => void; name: string; @@ -32,7 +43,9 @@ interface ProjectFormFieldsProps { copyFiles: string; setCopyFiles: (files: string) => void; error: string; + setError: (error: string) => void; projectId?: string; + onCreateProject?: (path: string, name: string) => void; } export function ProjectFormFields({ @@ -44,7 +57,6 @@ export function ProjectFormFields({ setShowFolderPicker, parentPath, setParentPath, - folderName, setFolderName, setName, name, @@ -57,7 +69,9 @@ export function ProjectFormFields({ copyFiles, setCopyFiles, error, + setError, projectId, + onCreateProject, }: ProjectFormFieldsProps) { const { system } = useUserSystem(); @@ -73,80 +87,291 @@ export function ProjectFormFields({ '#!/bin/bash\n# Add cleanup commands here...\n# This runs after coding agent execution', }; + // Repository loading state + const [allRepos, setAllRepos] = useState([]); + const [loading, setLoading] = useState(false); + const [reposError, setReposError] = useState(''); + const [showMoreOptions, setShowMoreOptions] = useState(false); + const [showRecentRepos, setShowRecentRepos] = useState(false); + + // Lazy-load repositories when the user navigates to the repo list + useEffect(() => { + if (!isEditing && showRecentRepos && !loading && allRepos.length === 0) { + loadRecentRepos(); + } + }, [isEditing, showRecentRepos]); + + const loadRecentRepos = async () => { + setLoading(true); + setReposError(''); + + try { + const discoveredRepos = await fileSystemApi.listGitRepos(); + setAllRepos(discoveredRepos); + } catch (err) { + setReposError('Failed to load repositories'); + console.error('Failed to load repos:', err); + } finally { + setLoading(false); + } + }; + return ( <> - {!isEditing && ( -
- -
-