From cdfb081cf8aeef29d0aa45ccb78cb24c3ae8c7a6 Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Tue, 13 Jan 2026 17:34:54 +0000 Subject: [PATCH] feat: copy-file autocomplete (#2004) * add repo file search endpoint and use for copy-file autocomplete * address feedback and fix i18n errors * remove unused i18n --- crates/deployment/src/lib.rs | 2 +- crates/local-deployment/src/lib.rs | 2 +- crates/server/src/bin/generate_types.rs | 1 + crates/server/src/routes/projects.rs | 2 +- crates/server/src/routes/repo.rs | 47 +++++- .../{file_search_cache.rs => file_search.rs} | 147 +++++++++++++++++ crates/services/src/services/mod.rs | 2 +- crates/services/src/services/project.rs | 154 +----------------- .../components/projects/CopyFilesField.tsx | 31 ---- .../ui-new/views/PreviewControls.tsx | 3 +- .../ui/multi-file-search-textarea.tsx | 38 +++-- frontend/src/i18n/locales/en/projects.json | 3 +- frontend/src/i18n/locales/en/settings.json | 3 +- frontend/src/i18n/locales/en/tasks.json | 7 +- frontend/src/i18n/locales/es/projects.json | 3 +- frontend/src/i18n/locales/es/settings.json | 3 +- frontend/src/i18n/locales/es/tasks.json | 7 +- frontend/src/i18n/locales/ja/projects.json | 3 +- frontend/src/i18n/locales/ja/settings.json | 3 +- frontend/src/i18n/locales/ja/tasks.json | 7 +- frontend/src/i18n/locales/ko/projects.json | 3 +- frontend/src/i18n/locales/ko/settings.json | 3 +- frontend/src/i18n/locales/ko/tasks.json | 7 +- .../src/i18n/locales/zh-Hans/projects.json | 3 +- .../src/i18n/locales/zh-Hans/settings.json | 3 +- frontend/src/i18n/locales/zh-Hans/tasks.json | 7 +- .../src/i18n/locales/zh-Hant/projects.json | 3 +- .../src/i18n/locales/zh-Hant/settings.json | 3 +- frontend/src/i18n/locales/zh-Hant/tasks.json | 7 +- frontend/src/lib/api.ts | 17 +- frontend/src/pages/settings/ReposSettings.tsx | 11 +- shared/types.ts | 2 + 32 files changed, 296 insertions(+), 241 deletions(-) rename crates/services/src/services/{file_search_cache.rs => file_search.rs} (76%) delete mode 100644 frontend/src/components/projects/CopyFilesField.tsx diff --git a/crates/deployment/src/lib.rs b/crates/deployment/src/lib.rs index d5227c06..63eaad59 100644 --- a/crates/deployment/src/lib.rs +++ b/crates/deployment/src/lib.rs @@ -22,7 +22,7 @@ use services::services::{ config::{Config, ConfigError}, container::{ContainerError, ContainerService}, events::{EventError, EventService}, - file_search_cache::FileSearchCache, + file_search::FileSearchCache, filesystem::{FilesystemError, FilesystemService}, filesystem_watcher::FilesystemWatcherError, git::{GitService, GitServiceError}, diff --git a/crates/local-deployment/src/lib.rs b/crates/local-deployment/src/lib.rs index c48dc861..b486bb3a 100644 --- a/crates/local-deployment/src/lib.rs +++ b/crates/local-deployment/src/lib.rs @@ -11,7 +11,7 @@ use services::services::{ config::{Config, load_config_from_file, save_config_to_file}, container::ContainerService, events::EventService, - file_search_cache::FileSearchCache, + file_search::FileSearchCache, filesystem::FilesystemService, git::GitService, image::ImageService, diff --git a/crates/server/src/bin/generate_types.rs b/crates/server/src/bin/generate_types.rs index fc50cb42..a07b6aa1 100644 --- a/crates/server/src/bin/generate_types.rs +++ b/crates/server/src/bin/generate_types.rs @@ -151,6 +151,7 @@ fn generate_types_content() -> String { server::routes::task_attempts::workspace_summary::WorkspaceSummaryResponse::decl(), services::services::filesystem::DirectoryEntry::decl(), services::services::filesystem::DirectoryListResponse::decl(), + services::services::file_search::SearchMode::decl(), services::services::config::Config::decl(), services::services::config::NotificationConfig::decl(), services::services::config::ThemeMode::decl(), diff --git a/crates/server/src/routes/projects.rs b/crates/server/src/routes/projects.rs index c08424db..cf228405 100644 --- a/crates/server/src/routes/projects.rs +++ b/crates/server/src/routes/projects.rs @@ -21,7 +21,7 @@ use deployment::Deployment; use futures_util::{SinkExt, StreamExt, TryStreamExt}; use serde::Deserialize; use services::services::{ - file_search_cache::SearchQuery, project::ProjectServiceError, + file_search::SearchQuery, project::ProjectServiceError, remote_client::CreateRemoteProjectPayload, }; use ts_rs::TS; diff --git a/crates/server/src/routes/repo.rs b/crates/server/src/routes/repo.rs index 476dcb9f..046936fa 100644 --- a/crates/server/src/routes/repo.rs +++ b/crates/server/src/routes/repo.rs @@ -1,13 +1,17 @@ use axum::{ Router, - extract::{Path, State}, + extract::{Path, Query, State}, + http::StatusCode, response::Json as ResponseJson, routing::{get, post}, }; -use db::models::repo::{Repo, UpdateRepo}; +use db::models::{ + project::SearchResult, + repo::{Repo, UpdateRepo}, +}; use deployment::Deployment; use serde::Deserialize; -use services::services::git::GitBranch; +use services::services::{file_search::SearchQuery, git::GitBranch}; use ts_rs::TS; use utils::response::ApiResponse; use uuid::Uuid; @@ -166,6 +170,42 @@ pub async fn open_repo_in_editor( } } +pub async fn search_repo( + State(deployment): State, + Path(repo_id): Path, + Query(search_query): Query, +) -> Result>>, StatusCode> { + if search_query.q.trim().is_empty() { + return Ok(ResponseJson(ApiResponse::error( + "Query parameter 'q' is required and cannot be empty", + ))); + } + + let repo = match deployment + .repo() + .get_by_id(&deployment.db().pool, repo_id) + .await + { + Ok(repo) => repo, + Err(e) => { + tracing::error!("Failed to get repo {}: {}", repo_id, e); + return Err(StatusCode::NOT_FOUND); + } + }; + + match deployment + .file_search_cache() + .search_repo(&repo.path, &search_query.q, search_query.mode) + .await + { + Ok(results) => Ok(ResponseJson(ApiResponse::success(results))), + Err(e) => { + tracing::error!("Failed to search files in repo {}: {}", repo_id, e); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + } +} + pub fn router() -> Router { Router::new() .route("/repos", get(get_repos).post(register_repo)) @@ -173,5 +213,6 @@ pub fn router() -> Router { .route("/repos/batch", post(get_repos_batch)) .route("/repos/{repo_id}", get(get_repo).put(update_repo)) .route("/repos/{repo_id}/branches", get(get_repo_branches)) + .route("/repos/{repo_id}/search", get(search_repo)) .route("/repos/{repo_id}/open-editor", post(open_repo_in_editor)) } diff --git a/crates/services/src/services/file_search_cache.rs b/crates/services/src/services/file_search.rs similarity index 76% rename from crates/services/src/services/file_search_cache.rs rename to crates/services/src/services/file_search.rs index 6f9ef854..0e0ba11f 100644 --- a/crates/services/src/services/file_search_cache.rs +++ b/crates/services/src/services/file_search.rs @@ -276,6 +276,153 @@ impl FileSearchCache { results } + /// Search files in a single repository with cache + fallback + pub async fn search_repo( + &self, + repo_path: &Path, + query: &str, + mode: SearchMode, + ) -> Result, String> { + let query = query.trim(); + if query.is_empty() { + return Ok(vec![]); + } + + // Try cache first + match self.search(repo_path, query, mode.clone()).await { + Ok(results) => Ok(results), + Err(CacheError::Miss) | Err(CacheError::BuildError(_)) => { + // Fall back to filesystem search + self.search_files_no_cache(repo_path, query, mode).await + } + } + } + + /// Fallback filesystem search when cache is not available + async fn search_files_no_cache( + &self, + repo_path: &Path, + query: &str, + mode: SearchMode, + ) -> Result, String> { + if !repo_path.exists() { + return Err(format!("Path not found: {:?}", repo_path)); + } + + let mut results = Vec::new(); + let query_lower = query.to_lowercase(); + + let walker = match mode { + SearchMode::Settings => { + // Settings mode: Include ignored files but exclude performance killers + WalkBuilder::new(repo_path) + .git_ignore(false) + .git_global(false) + .git_exclude(false) + .hidden(false) + .filter_entry(|entry| { + let name = entry.file_name().to_string_lossy(); + name != ".git" + && name != "node_modules" + && name != "target" + && name != "dist" + && name != "build" + }) + .build() + } + SearchMode::TaskForm => WalkBuilder::new(repo_path) + .git_ignore(true) + .git_global(true) + .git_exclude(true) + .hidden(false) + .filter_entry(|entry| { + let name = entry.file_name().to_string_lossy(); + name != ".git" + }) + .build(), + }; + + for result in walker { + let entry = match result { + Ok(e) => e, + Err(_) => continue, + }; + let path = entry.path(); + + // Skip the root directory itself + if path == repo_path { + continue; + } + + let relative_path = match path.strip_prefix(repo_path) { + Ok(p) => p, + Err(_) => continue, + }; + let relative_path_str = relative_path.to_string_lossy().to_lowercase(); + + let file_name = path + .file_name() + .map(|name| name.to_string_lossy().to_lowercase()) + .unwrap_or_default(); + + if file_name.contains(&query_lower) { + results.push(SearchResult { + path: relative_path.to_string_lossy().to_string(), + is_file: path.is_file(), + match_type: SearchMatchType::FileName, + score: 0, + }); + } else if relative_path_str.contains(&query_lower) { + let match_type = if path + .parent() + .and_then(|p| p.file_name()) + .map(|name| name.to_string_lossy().to_lowercase()) + .unwrap_or_default() + .contains(&query_lower) + { + SearchMatchType::DirectoryName + } else { + SearchMatchType::FullPath + }; + + results.push(SearchResult { + path: relative_path.to_string_lossy().to_string(), + is_file: path.is_file(), + match_type, + score: 0, + }); + } + } + + // Apply git history-based ranking + match self.file_ranker.get_stats(repo_path).await { + Ok(stats) => { + self.file_ranker.rerank(&mut results, &stats); + // Populate scores for sorted results + for result in &mut results { + result.score = self.file_ranker.calculate_score(result, &stats); + } + } + Err(_) => { + // Fallback to basic priority sorting + results.sort_by(|a, b| { + let priority = |match_type: &SearchMatchType| match match_type { + SearchMatchType::FileName => 0, + SearchMatchType::DirectoryName => 1, + SearchMatchType::FullPath => 2, + }; + + priority(&a.match_type) + .cmp(&priority(&b.match_type)) + .then_with(|| a.path.cmp(&b.path)) + }); + } + } + + results.truncate(10); + Ok(results) + } + /// Build cache entry for a repository async fn build_repo_cache(&self, repo_path: &Path) -> Result { let repo_path_buf = repo_path.to_path_buf(); diff --git a/crates/services/src/services/mod.rs b/crates/services/src/services/mod.rs index 5ae78b0b..3b9a8292 100644 --- a/crates/services/src/services/mod.rs +++ b/crates/services/src/services/mod.rs @@ -6,7 +6,7 @@ pub mod container; pub mod diff_stream; pub mod events; pub mod file_ranker; -pub mod file_search_cache; +pub mod file_search; pub mod filesystem; pub mod filesystem_watcher; pub mod git; diff --git a/crates/services/src/services/project.rs b/crates/services/src/services/project.rs index a11acfa9..75a499d7 100644 --- a/crates/services/src/services/project.rs +++ b/crates/services/src/services/project.rs @@ -9,15 +9,13 @@ use db::models::{ repo::Repo, task::Task, }; -use ignore::WalkBuilder; use sqlx::SqlitePool; use thiserror::Error; use utils::api::projects::RemoteProject; use uuid::Uuid; use super::{ - file_ranker::FileRanker, - file_search_cache::{CacheError, FileSearchCache, SearchMode, SearchQuery}, + file_search::{FileSearchCache, SearchQuery}, repo::{RepoError, RepoService}, share::ShareError, }; @@ -277,10 +275,11 @@ impl ProjectService { .map(|repo| { let repo_name = repo.name.clone(); let repo_path = repo.path.clone(); - let query = query.clone(); + let mode = query.mode.clone(); + let query_str = query_str.to_string(); async move { - let results = self - .search_single_repo(cache, &repo_path, &query) + let results = cache + .search_repo(&repo_path, &query_str, mode) .await .unwrap_or_else(|e| { tracing::warn!("Search failed for repo {}: {}", repo_name, e); @@ -319,147 +318,4 @@ impl ProjectService { all_results.truncate(10); Ok(all_results) } - - async fn search_single_repo( - &self, - cache: &FileSearchCache, - repo_path: &Path, - query: &SearchQuery, - ) -> Result> { - let query_str = query.q.trim(); - if query_str.is_empty() { - return Ok(vec![]); - } - - // Try cache first - match cache.search(repo_path, query_str, query.mode.clone()).await { - Ok(results) => Ok(results), - Err(CacheError::Miss) | Err(CacheError::BuildError(_)) => { - // Fall back to filesystem search - self.search_files_in_repo(repo_path, query_str, query.mode.clone()) - .await - } - } - } - - async fn search_files_in_repo( - &self, - repo_path: &Path, - query: &str, - mode: SearchMode, - ) -> Result> { - if !repo_path.exists() { - return Err(ProjectServiceError::PathNotFound(repo_path.to_path_buf())); - } - - let mut results = Vec::new(); - let query_lower = query.to_lowercase(); - - let walker = match mode { - SearchMode::Settings => { - // Settings mode: Include ignored files but exclude performance killers - WalkBuilder::new(repo_path) - .git_ignore(false) - .git_global(false) - .git_exclude(false) - .hidden(false) - .filter_entry(|entry| { - let name = entry.file_name().to_string_lossy(); - name != ".git" - && name != "node_modules" - && name != "target" - && name != "dist" - && name != "build" - }) - .build() - } - SearchMode::TaskForm => WalkBuilder::new(repo_path) - .git_ignore(true) - .git_global(true) - .git_exclude(true) - .hidden(false) - .filter_entry(|entry| { - let name = entry.file_name().to_string_lossy(); - name != ".git" - }) - .build(), - }; - - for result in walker { - let entry = result.map_err(std::io::Error::other)?; - let path = entry.path(); - - // Skip the root directory itself - if path == repo_path { - continue; - } - - let relative_path = path - .strip_prefix(repo_path) - .map_err(std::io::Error::other)?; - let relative_path_str = relative_path.to_string_lossy().to_lowercase(); - - let file_name = path - .file_name() - .map(|name| name.to_string_lossy().to_lowercase()) - .unwrap_or_default(); - - if file_name.contains(&query_lower) { - results.push(SearchResult { - path: relative_path.to_string_lossy().to_string(), - is_file: path.is_file(), - match_type: SearchMatchType::FileName, - score: 0, - }); - } else if relative_path_str.contains(&query_lower) { - let match_type = if path - .parent() - .and_then(|p| p.file_name()) - .map(|name| name.to_string_lossy().to_lowercase()) - .unwrap_or_default() - .contains(&query_lower) - { - SearchMatchType::DirectoryName - } else { - SearchMatchType::FullPath - }; - - results.push(SearchResult { - path: relative_path.to_string_lossy().to_string(), - is_file: path.is_file(), - match_type, - score: 0, - }); - } - } - - // Apply git history-based ranking - let file_ranker = FileRanker::new(); - match file_ranker.get_stats(repo_path).await { - Ok(stats) => { - file_ranker.rerank(&mut results, &stats); - // Populate scores for sorted results - for result in &mut results { - result.score = file_ranker.calculate_score(result, &stats); - } - } - Err(_) => { - // Fallback to basic priority sorting - results.sort_by(|a, b| { - let priority = |match_type: &SearchMatchType| match match_type { - SearchMatchType::FileName => 0, - SearchMatchType::DirectoryName => 1, - SearchMatchType::FullPath => 2, - }; - - priority(&a.match_type) - .cmp(&priority(&b.match_type)) - .then_with(|| a.path.cmp(&b.path)) - }); - } - } - - results.truncate(10); - Ok(results) - } } diff --git a/frontend/src/components/projects/CopyFilesField.tsx b/frontend/src/components/projects/CopyFilesField.tsx deleted file mode 100644 index 9aa313b1..00000000 --- a/frontend/src/components/projects/CopyFilesField.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { MultiFileSearchTextarea } from '@/components/ui/multi-file-search-textarea'; - -interface CopyFilesFieldProps { - value: string; - onChange: (value: string) => void; - projectId: string; - disabled?: boolean; -} - -export function CopyFilesField({ - value, - onChange, - projectId, - disabled = false, -}: CopyFilesFieldProps) { - const { t } = useTranslation('projects'); - - return ( - - ); -} diff --git a/frontend/src/components/ui-new/views/PreviewControls.tsx b/frontend/src/components/ui-new/views/PreviewControls.tsx index ff9ca89f..7cc8647f 100644 --- a/frontend/src/components/ui-new/views/PreviewControls.tsx +++ b/frontend/src/components/ui-new/views/PreviewControls.tsx @@ -75,7 +75,8 @@ export function PreviewControls({ )} onClick={() => onTabChange(process.id)} > - {getDevServerWorkingDir(process) ?? 'Dev Server'} + {getDevServerWorkingDir(process) ?? + t('preview.browser.devServerFallback')} ))} diff --git a/frontend/src/components/ui/multi-file-search-textarea.tsx b/frontend/src/components/ui/multi-file-search-textarea.tsx index 4e2382cc..59ac26fa 100644 --- a/frontend/src/components/ui/multi-file-search-textarea.tsx +++ b/frontend/src/components/ui/multi-file-search-textarea.tsx @@ -2,7 +2,7 @@ import { KeyboardEvent, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { AutoExpandingTextarea } from '@/components/ui/auto-expanding-textarea'; import { usePortalContainer } from '@/contexts/PortalContainerContext'; -import { projectsApi } from '@/lib/api'; +import { projectsApi, repoApi } from '@/lib/api'; import type { SearchResult } from 'shared/types'; @@ -17,7 +17,10 @@ interface MultiFileSearchTextareaProps { rows?: number; disabled?: boolean; className?: string; - projectId: string; + /** Project ID for project-level file search (searches across all repos in project) */ + projectId?: string; + /** Repo ID for repo-level file search (searches within a single repo) */ + repoId?: string; onKeyDown?: (e: React.KeyboardEvent) => void; maxRows?: number; } @@ -30,9 +33,13 @@ export function MultiFileSearchTextarea({ disabled = false, className, projectId, + repoId, onKeyDown, maxRows = 10, }: MultiFileSearchTextareaProps) { + // Require at least one of projectId or repoId + const searchId = projectId || repoId; + const searchType = projectId ? 'project' : 'repo'; const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [showDropdown, setShowDropdown] = useState(false); @@ -48,9 +55,16 @@ export function MultiFileSearchTextarea({ const itemRefs = useRef>(new Map()); const portalContainer = usePortalContainer(); + useEffect(() => { + searchCacheRef.current.clear(); + setSearchQuery(''); + setSearchResults([]); + setShowDropdown(false); + }, [searchId]); + // Search for files when query changes useEffect(() => { - if (!searchQuery || !projectId || searchQuery.length < 2) { + if (!searchQuery || !searchId || searchQuery.length < 2) { setSearchResults([]); setShowDropdown(false); return; @@ -77,14 +91,14 @@ export function MultiFileSearchTextarea({ abortControllerRef.current = abortController; try { - const result = await projectsApi.searchFiles( - projectId, - searchQuery, - 'settings', - { - signal: abortController.signal, - } - ); + const result = + searchType === 'project' + ? await projectsApi.searchFiles(searchId, searchQuery, 'settings', { + signal: abortController.signal, + }) + : await repoApi.searchFiles(searchId, searchQuery, 'settings', { + signal: abortController.signal, + }); // Only process if this request wasn't aborted if (!abortController.signal.aborted) { @@ -118,7 +132,7 @@ export function MultiFileSearchTextarea({ abortControllerRef.current.abort(); } }; - }, [searchQuery, projectId]); + }, [searchQuery, searchId, searchType]); // Find current token boundaries based on cursor position const findCurrentToken = (text: string, cursorPosition: number) => { diff --git a/frontend/src/i18n/locales/en/projects.json b/frontend/src/i18n/locales/en/projects.json index 9f951ae4..05a3c1ff 100644 --- a/frontend/src/i18n/locales/en/projects.json +++ b/frontend/src/i18n/locales/en/projects.json @@ -55,6 +55,5 @@ "projectNotFound": "The project you're looking for doesn't exist or has been deleted.", "viewProject": "View Project", "openInIDE": "Open in IDE", - "createdDate": "Created {{date}}", - "copyFilesPlaceholderWithSearch": "File paths or glob patterns (e.g., .env, config/*.json)" + "createdDate": "Created {{date}}" } diff --git a/frontend/src/i18n/locales/en/settings.json b/frontend/src/i18n/locales/en/settings.json index 6a31dd4f..f2ecb90f 100644 --- a/frontend/src/i18n/locales/en/settings.json +++ b/frontend/src/i18n/locales/en/settings.json @@ -380,7 +380,8 @@ }, "copyFiles": { "label": "Copy Files", - "helper": "Comma-separated list of files to copy from the original repository directory to the worktree. Useful for environment files like .env. Make sure these are gitignored!" + "helper": "Comma-separated list of files to copy from the original repository directory to the worktree. Useful for environment files like .env. Make sure these are gitignored!", + "placeholder": "File paths or glob patterns (e.g., .env, config/*.json)" }, "devServer": { "label": "Dev Server Script", diff --git a/frontend/src/i18n/locales/en/tasks.json b/frontend/src/i18n/locales/en/tasks.json index 2b1aa168..b568bc19 100644 --- a/frontend/src/i18n/locales/en/tasks.json +++ b/frontend/src/i18n/locales/en/tasks.json @@ -137,9 +137,10 @@ }, "browser": { "title": "Dev Server Preview", - "startButton": "Start Dev Server", - "stopButton": "Stop", - "startingButton": "Start" + "devServerFallback": "Dev Server" + }, + "urlInput": { + "placeholder": "Enter URL..." } }, "diff": { diff --git a/frontend/src/i18n/locales/es/projects.json b/frontend/src/i18n/locales/es/projects.json index 9b92f813..d188afb8 100644 --- a/frontend/src/i18n/locales/es/projects.json +++ b/frontend/src/i18n/locales/es/projects.json @@ -55,6 +55,5 @@ "projectNotFound": "El proyecto que buscas no existe o ha sido eliminado.", "viewProject": "Ver Proyecto", "openInIDE": "Abrir en IDE", - "createdDate": "Creado {{date}}", - "copyFilesPlaceholderWithSearch": "Escribe una ruta o patrón glob (.env, config/*.json)" + "createdDate": "Creado {{date}}" } diff --git a/frontend/src/i18n/locales/es/settings.json b/frontend/src/i18n/locales/es/settings.json index 687fcff4..54e0182f 100644 --- a/frontend/src/i18n/locales/es/settings.json +++ b/frontend/src/i18n/locales/es/settings.json @@ -380,7 +380,8 @@ }, "copyFiles": { "label": "Copiar Archivos", - "helper": "Lista separada por comas de archivos para copiar del directorio del repositorio original al worktree. Útil para archivos de entorno como .env. ¡Asegúrate de que estén en gitignore!" + "helper": "Lista separada por comas de archivos para copiar del directorio del repositorio original al worktree. Útil para archivos de entorno como .env. ¡Asegúrate de que estén en gitignore!", + "placeholder": "Rutas de archivos o patrones glob (ej., .env, config/*.json)" }, "devServer": { "label": "Script del Servidor de Desarrollo", diff --git a/frontend/src/i18n/locales/es/tasks.json b/frontend/src/i18n/locales/es/tasks.json index 62a8c855..06432df5 100644 --- a/frontend/src/i18n/locales/es/tasks.json +++ b/frontend/src/i18n/locales/es/tasks.json @@ -373,9 +373,10 @@ "noDevScriptHint": "Agrega un script de desarrollo en la configuración del proyecto para habilitar la vista previa.", "browser": { "title": "Vista previa del servidor de desarrollo", - "startButton": "Iniciar servidor de desarrollo", - "stopButton": "Detener", - "startingButton": "Iniciar" + "devServerFallback": "Servidor de desarrollo" + }, + "urlInput": { + "placeholder": "Ingresar URL..." }, "noServer": { "companionLink": "Ver guía de instalación", diff --git a/frontend/src/i18n/locales/ja/projects.json b/frontend/src/i18n/locales/ja/projects.json index 33283127..422b2718 100644 --- a/frontend/src/i18n/locales/ja/projects.json +++ b/frontend/src/i18n/locales/ja/projects.json @@ -55,6 +55,5 @@ "projectNotFound": "お探しのプロジェクトは存在しないか、削除されました。", "viewProject": "プロジェクトを表示", "openInIDE": "IDEで開く", - "createdDate": "作成日 {{date}}", - "copyFilesPlaceholderWithSearch": "パスまたはglobパターンを入力 (.env, config/*.json)" + "createdDate": "作成日 {{date}}" } diff --git a/frontend/src/i18n/locales/ja/settings.json b/frontend/src/i18n/locales/ja/settings.json index 4af841d9..7f80dfe0 100644 --- a/frontend/src/i18n/locales/ja/settings.json +++ b/frontend/src/i18n/locales/ja/settings.json @@ -380,7 +380,8 @@ }, "copyFiles": { "label": "ファイルをコピー", - "helper": "元のリポジトリディレクトリからワークツリーにコピーするファイルのカンマ区切りリスト。.envなどの環境ファイルに役立ちます。gitignoreされていることを確認してください!" + "helper": "元のリポジトリディレクトリからワークツリーにコピーするファイルのカンマ区切りリスト。.envなどの環境ファイルに役立ちます。gitignoreされていることを確認してください!", + "placeholder": "ファイルパスまたはglobパターン(例:.env、config/*.json)" }, "devServer": { "label": "開発サーバースクリプト", diff --git a/frontend/src/i18n/locales/ja/tasks.json b/frontend/src/i18n/locales/ja/tasks.json index 10a78dcf..54f1408a 100644 --- a/frontend/src/i18n/locales/ja/tasks.json +++ b/frontend/src/i18n/locales/ja/tasks.json @@ -371,9 +371,10 @@ }, "browser": { "title": "開発サーバープレビュー", - "startButton": "開発サーバーを開始", - "stopButton": "停止", - "startingButton": "開始" + "devServerFallback": "開発サーバー" + }, + "urlInput": { + "placeholder": "URLを入力..." }, "noDevScript": "開発スクリプトが設定されていません", "noDevScriptHint": "プレビューを有効にするには、プロジェクト設定で開発スクリプトを追加してください。", diff --git a/frontend/src/i18n/locales/ko/projects.json b/frontend/src/i18n/locales/ko/projects.json index 7789e472..f4322fa2 100644 --- a/frontend/src/i18n/locales/ko/projects.json +++ b/frontend/src/i18n/locales/ko/projects.json @@ -55,6 +55,5 @@ "projectNotFound": "찾으시는 프로젝트가 존재하지 않거나 삭제되었습니다.", "viewProject": "프로젝트 보기", "openInIDE": "IDE에서 열기", - "createdDate": "생성일 {{date}}", - "copyFilesPlaceholderWithSearch": "경로 또는 glob 패턴 입력 (.env, config/*.json)" + "createdDate": "생성일 {{date}}" } diff --git a/frontend/src/i18n/locales/ko/settings.json b/frontend/src/i18n/locales/ko/settings.json index f1e84d99..9e8c8122 100644 --- a/frontend/src/i18n/locales/ko/settings.json +++ b/frontend/src/i18n/locales/ko/settings.json @@ -380,7 +380,8 @@ }, "copyFiles": { "label": "파일 복사", - "helper": "원래 저장소 디렉토리에서 워크트리로 복사할 파일의 쉼표로 구분된 목록입니다. .env와 같은 환경 파일에 유용합니다. gitignore되었는지 확인하세요!" + "helper": "원래 저장소 디렉토리에서 워크트리로 복사할 파일의 쉼표로 구분된 목록입니다. .env와 같은 환경 파일에 유용합니다. gitignore되었는지 확인하세요!", + "placeholder": "파일 경로 또는 glob 패턴 (예: .env, config/*.json)" }, "devServer": { "label": "개발 서버 스크립트", diff --git a/frontend/src/i18n/locales/ko/tasks.json b/frontend/src/i18n/locales/ko/tasks.json index aa287e9a..ec4a5a1c 100644 --- a/frontend/src/i18n/locales/ko/tasks.json +++ b/frontend/src/i18n/locales/ko/tasks.json @@ -413,9 +413,10 @@ }, "browser": { "title": "개발 서버 미리보기", - "startButton": "개발 서버 시작", - "stopButton": "중지", - "startingButton": "시작" + "devServerFallback": "개발 서버" + }, + "urlInput": { + "placeholder": "URL 입력..." }, "noDevScript": "개발 스크립트가 설정되지 않았습니다", "noDevScriptHint": "미리보기를 활성화하려면 프로젝트 설정에서 개발 스크립트를 추가하세요." diff --git a/frontend/src/i18n/locales/zh-Hans/projects.json b/frontend/src/i18n/locales/zh-Hans/projects.json index 0aadc6f6..ab60a69a 100644 --- a/frontend/src/i18n/locales/zh-Hans/projects.json +++ b/frontend/src/i18n/locales/zh-Hans/projects.json @@ -55,6 +55,5 @@ "projectNotFound": "您查找的项目不存在或已被删除。", "viewProject": "查看项目", "openInIDE": "在 IDE 中打开", - "createdDate": "创建于 {{date}}", - "copyFilesPlaceholderWithSearch": "文件路径或 glob 模式(例如:.env、config/*.json)" + "createdDate": "创建于 {{date}}" } diff --git a/frontend/src/i18n/locales/zh-Hans/settings.json b/frontend/src/i18n/locales/zh-Hans/settings.json index 45567fc2..b36df792 100644 --- a/frontend/src/i18n/locales/zh-Hans/settings.json +++ b/frontend/src/i18n/locales/zh-Hans/settings.json @@ -380,7 +380,8 @@ }, "copyFiles": { "label": "复制文件", - "helper": "要从原始仓库目录复制到工作树的文件的逗号分隔列表。对 .env 等环境文件很有用。确保这些文件被 gitignore!" + "helper": "要从原始仓库目录复制到工作树的文件的逗号分隔列表。对 .env 等环境文件很有用。确保这些文件被 gitignore!", + "placeholder": "文件路径或 glob 模式(例如:.env、config/*.json)" }, "devServer": { "label": "开发服务器脚本", diff --git a/frontend/src/i18n/locales/zh-Hans/tasks.json b/frontend/src/i18n/locales/zh-Hans/tasks.json index dfbaadae..eff8316f 100644 --- a/frontend/src/i18n/locales/zh-Hans/tasks.json +++ b/frontend/src/i18n/locales/zh-Hans/tasks.json @@ -130,9 +130,10 @@ "noDevScriptHint": "在项目设置中添加开发脚本以启用预览。", "browser": { "title": "开发服务器预览", - "startButton": "启动开发服务器", - "stopButton": "停止", - "startingButton": "启动" + "devServerFallback": "开发服务器" + }, + "urlInput": { + "placeholder": "输入 URL..." }, "iframe": { "title": "开发服务器预览" diff --git a/frontend/src/i18n/locales/zh-Hant/projects.json b/frontend/src/i18n/locales/zh-Hant/projects.json index 5815e7ea..0eb42f96 100644 --- a/frontend/src/i18n/locales/zh-Hant/projects.json +++ b/frontend/src/i18n/locales/zh-Hant/projects.json @@ -55,6 +55,5 @@ "projectNotFound": "您要查找的專案不存在或已被刪除。", "viewProject": "查看專案", "openInIDE": "在 IDE 中開啟", - "createdDate": "建立於 {{date}}", - "copyFilesPlaceholderWithSearch": "檔案路徑或 glob 模式(例如:.env、config/*.json)" + "createdDate": "建立於 {{date}}" } diff --git a/frontend/src/i18n/locales/zh-Hant/settings.json b/frontend/src/i18n/locales/zh-Hant/settings.json index b266d7df..d42da488 100644 --- a/frontend/src/i18n/locales/zh-Hant/settings.json +++ b/frontend/src/i18n/locales/zh-Hant/settings.json @@ -380,7 +380,8 @@ }, "copyFiles": { "label": "複製檔案", - "helper": "要從原始儲存庫目錄複製到工作樹的檔案清單(以逗號分隔)。適合用於 .env 等環境檔案。請確保這些檔案已加入 gitignore!" + "helper": "要從原始儲存庫目錄複製到工作樹的檔案清單(以逗號分隔)。適合用於 .env 等環境檔案。請確保這些檔案已加入 gitignore!", + "placeholder": "檔案路徑或 glob 模式(例如:.env、config/*.json)" }, "devServer": { "label": "開發伺服器腳本", diff --git a/frontend/src/i18n/locales/zh-Hant/tasks.json b/frontend/src/i18n/locales/zh-Hant/tasks.json index b5596113..675d529d 100644 --- a/frontend/src/i18n/locales/zh-Hant/tasks.json +++ b/frontend/src/i18n/locales/zh-Hant/tasks.json @@ -130,9 +130,10 @@ "noDevScriptHint": "在專案設定中新增開發指令碼以啟用預覽。", "browser": { "title": "開發伺服器預覽", - "startButton": "啟動開發伺服器", - "stopButton": "停止", - "startingButton": "啟動" + "devServerFallback": "開發伺服器" + }, + "urlInput": { + "placeholder": "輸入 URL..." }, "iframe": { "title": "開發伺服器預覽" diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 85f64fcd..0fafbf94 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -22,6 +22,7 @@ import { CreateProject, CreateProjectRepo, UpdateRepo, + SearchMode, SearchResult, ShareTaskResponse, Task, @@ -283,7 +284,7 @@ export const projectsApi = { searchFiles: async ( id: string, query: string, - mode?: string, + mode?: SearchMode, options?: RequestInit ): Promise => { const modeParam = mode ? `&mode=${encodeURIComponent(mode)}` : ''; @@ -895,6 +896,20 @@ export const repoApi = { }); return handleApiResponse(response); }, + + searchFiles: async ( + repoId: string, + query: string, + mode?: SearchMode, + options?: RequestInit + ): Promise => { + const modeParam = mode ? `&mode=${encodeURIComponent(mode)}` : ''; + const response = await makeRequest( + `/api/repos/${repoId}/search?q=${encodeURIComponent(query)}${modeParam}`, + options + ); + return handleApiResponse(response); + }, }; // Config APIs (backwards compatible) diff --git a/frontend/src/pages/settings/ReposSettings.tsx b/frontend/src/pages/settings/ReposSettings.tsx index ef242678..5de57f83 100644 --- a/frontend/src/pages/settings/ReposSettings.tsx +++ b/frontend/src/pages/settings/ReposSettings.tsx @@ -24,6 +24,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert'; import { Loader2 } from 'lucide-react'; import { useScriptPlaceholders } from '@/hooks/useScriptPlaceholders'; import { AutoExpandingTextarea } from '@/components/ui/auto-expanding-textarea'; +import { MultiFileSearchTextarea } from '@/components/ui/multi-file-search-textarea'; import { repoApi } from '@/lib/api'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import type { Repo, UpdateRepo } from 'shared/types'; @@ -422,12 +423,14 @@ export function ReposSettings() { - updateDraft({ copy_files: e.target.value })} - placeholder=".env, .env.local" + onChange={(value) => updateDraft({ copy_files: value })} + placeholder={t( + 'settings.repos.scripts.copyFiles.placeholder' + )} maxRows={6} + repoId={selectedRepo.id} className="w-full px-3 py-2 border border-input bg-background text-foreground rounded-md focus:outline-none focus:ring-2 focus:ring-ring font-mono" />

diff --git a/shared/types.ts b/shared/types.ts index aaf01abd..89eac25c 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -370,6 +370,8 @@ export type DirectoryEntry = { name: string, path: string, is_directory: boolean export type DirectoryListResponse = { entries: Array, current_path: string, }; +export type SearchMode = "taskform" | "settings"; + export type Config = { config_version: string, theme: ThemeMode, executor_profile: ExecutorProfileId, disclaimer_acknowledged: boolean, onboarding_acknowledged: boolean, notifications: NotificationConfig, editor: EditorConfig, github: GitHubConfig, analytics_enabled: boolean, workspace_dir: string | null, last_app_version: string | null, show_release_notes: boolean, language: UiLanguage, git_branch_prefix: string, showcases: ShowcaseState, pr_auto_description_enabled: boolean, pr_auto_description_prompt: string | null, }; export type NotificationConfig = { sound_enabled: boolean, push_enabled: boolean, sound_file: SoundFile, };