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
This commit is contained in:
committed by
GitHub
parent
5502a4cad6
commit
cdfb081cf8
@@ -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},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<DeploymentImpl>,
|
||||
Path(repo_id): Path<Uuid>,
|
||||
Query(search_query): Query<SearchQuery>,
|
||||
) -> Result<ResponseJson<ApiResponse<Vec<SearchResult>>>, 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<DeploymentImpl> {
|
||||
Router::new()
|
||||
.route("/repos", get(get_repos).post(register_repo))
|
||||
@@ -173,5 +213,6 @@ pub fn router() -> Router<DeploymentImpl> {
|
||||
.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))
|
||||
}
|
||||
|
||||
@@ -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<Vec<SearchResult>, 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<Vec<SearchResult>, 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<CachedRepo, String> {
|
||||
let repo_path_buf = repo_path.to_path_buf();
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Vec<SearchResult>> {
|
||||
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<Vec<SearchResult>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<MultiFileSearchTextarea
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={t('copyFilesPlaceholderWithSearch')}
|
||||
rows={3}
|
||||
disabled={disabled}
|
||||
className="w-full px-3 py-2 text-sm border border-input bg-background text-foreground disabled:opacity-50 rounded-md resize-vertical focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
projectId={projectId}
|
||||
maxRows={6}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -75,7 +75,8 @@ export function PreviewControls({
|
||||
)}
|
||||
onClick={() => onTabChange(process.id)}
|
||||
>
|
||||
{getDevServerWorkingDir(process) ?? 'Dev Server'}
|
||||
{getDevServerWorkingDir(process) ??
|
||||
t('preview.browser.devServerFallback')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -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<FileSearchResult[]>([]);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
@@ -48,9 +55,16 @@ export function MultiFileSearchTextarea({
|
||||
const itemRefs = useRef<Map<number, HTMLDivElement>>(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) => {
|
||||
|
||||
@@ -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}}"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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}}"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -55,6 +55,5 @@
|
||||
"projectNotFound": "お探しのプロジェクトは存在しないか、削除されました。",
|
||||
"viewProject": "プロジェクトを表示",
|
||||
"openInIDE": "IDEで開く",
|
||||
"createdDate": "作成日 {{date}}",
|
||||
"copyFilesPlaceholderWithSearch": "パスまたはglobパターンを入力 (.env, config/*.json)"
|
||||
"createdDate": "作成日 {{date}}"
|
||||
}
|
||||
|
||||
@@ -380,7 +380,8 @@
|
||||
},
|
||||
"copyFiles": {
|
||||
"label": "ファイルをコピー",
|
||||
"helper": "元のリポジトリディレクトリからワークツリーにコピーするファイルのカンマ区切りリスト。.envなどの環境ファイルに役立ちます。gitignoreされていることを確認してください!"
|
||||
"helper": "元のリポジトリディレクトリからワークツリーにコピーするファイルのカンマ区切りリスト。.envなどの環境ファイルに役立ちます。gitignoreされていることを確認してください!",
|
||||
"placeholder": "ファイルパスまたはglobパターン(例:.env、config/*.json)"
|
||||
},
|
||||
"devServer": {
|
||||
"label": "開発サーバースクリプト",
|
||||
|
||||
@@ -371,9 +371,10 @@
|
||||
},
|
||||
"browser": {
|
||||
"title": "開発サーバープレビュー",
|
||||
"startButton": "開発サーバーを開始",
|
||||
"stopButton": "停止",
|
||||
"startingButton": "開始"
|
||||
"devServerFallback": "開発サーバー"
|
||||
},
|
||||
"urlInput": {
|
||||
"placeholder": "URLを入力..."
|
||||
},
|
||||
"noDevScript": "開発スクリプトが設定されていません",
|
||||
"noDevScriptHint": "プレビューを有効にするには、プロジェクト設定で開発スクリプトを追加してください。",
|
||||
|
||||
@@ -55,6 +55,5 @@
|
||||
"projectNotFound": "찾으시는 프로젝트가 존재하지 않거나 삭제되었습니다.",
|
||||
"viewProject": "프로젝트 보기",
|
||||
"openInIDE": "IDE에서 열기",
|
||||
"createdDate": "생성일 {{date}}",
|
||||
"copyFilesPlaceholderWithSearch": "경로 또는 glob 패턴 입력 (.env, config/*.json)"
|
||||
"createdDate": "생성일 {{date}}"
|
||||
}
|
||||
|
||||
@@ -380,7 +380,8 @@
|
||||
},
|
||||
"copyFiles": {
|
||||
"label": "파일 복사",
|
||||
"helper": "원래 저장소 디렉토리에서 워크트리로 복사할 파일의 쉼표로 구분된 목록입니다. .env와 같은 환경 파일에 유용합니다. gitignore되었는지 확인하세요!"
|
||||
"helper": "원래 저장소 디렉토리에서 워크트리로 복사할 파일의 쉼표로 구분된 목록입니다. .env와 같은 환경 파일에 유용합니다. gitignore되었는지 확인하세요!",
|
||||
"placeholder": "파일 경로 또는 glob 패턴 (예: .env, config/*.json)"
|
||||
},
|
||||
"devServer": {
|
||||
"label": "개발 서버 스크립트",
|
||||
|
||||
@@ -413,9 +413,10 @@
|
||||
},
|
||||
"browser": {
|
||||
"title": "개발 서버 미리보기",
|
||||
"startButton": "개발 서버 시작",
|
||||
"stopButton": "중지",
|
||||
"startingButton": "시작"
|
||||
"devServerFallback": "개발 서버"
|
||||
},
|
||||
"urlInput": {
|
||||
"placeholder": "URL 입력..."
|
||||
},
|
||||
"noDevScript": "개발 스크립트가 설정되지 않았습니다",
|
||||
"noDevScriptHint": "미리보기를 활성화하려면 프로젝트 설정에서 개발 스크립트를 추가하세요."
|
||||
|
||||
@@ -55,6 +55,5 @@
|
||||
"projectNotFound": "您查找的项目不存在或已被删除。",
|
||||
"viewProject": "查看项目",
|
||||
"openInIDE": "在 IDE 中打开",
|
||||
"createdDate": "创建于 {{date}}",
|
||||
"copyFilesPlaceholderWithSearch": "文件路径或 glob 模式(例如:.env、config/*.json)"
|
||||
"createdDate": "创建于 {{date}}"
|
||||
}
|
||||
|
||||
@@ -380,7 +380,8 @@
|
||||
},
|
||||
"copyFiles": {
|
||||
"label": "复制文件",
|
||||
"helper": "要从原始仓库目录复制到工作树的文件的逗号分隔列表。对 .env 等环境文件很有用。确保这些文件被 gitignore!"
|
||||
"helper": "要从原始仓库目录复制到工作树的文件的逗号分隔列表。对 .env 等环境文件很有用。确保这些文件被 gitignore!",
|
||||
"placeholder": "文件路径或 glob 模式(例如:.env、config/*.json)"
|
||||
},
|
||||
"devServer": {
|
||||
"label": "开发服务器脚本",
|
||||
|
||||
@@ -130,9 +130,10 @@
|
||||
"noDevScriptHint": "在项目设置中添加开发脚本以启用预览。",
|
||||
"browser": {
|
||||
"title": "开发服务器预览",
|
||||
"startButton": "启动开发服务器",
|
||||
"stopButton": "停止",
|
||||
"startingButton": "启动"
|
||||
"devServerFallback": "开发服务器"
|
||||
},
|
||||
"urlInput": {
|
||||
"placeholder": "输入 URL..."
|
||||
},
|
||||
"iframe": {
|
||||
"title": "开发服务器预览"
|
||||
|
||||
@@ -55,6 +55,5 @@
|
||||
"projectNotFound": "您要查找的專案不存在或已被刪除。",
|
||||
"viewProject": "查看專案",
|
||||
"openInIDE": "在 IDE 中開啟",
|
||||
"createdDate": "建立於 {{date}}",
|
||||
"copyFilesPlaceholderWithSearch": "檔案路徑或 glob 模式(例如:.env、config/*.json)"
|
||||
"createdDate": "建立於 {{date}}"
|
||||
}
|
||||
|
||||
@@ -380,7 +380,8 @@
|
||||
},
|
||||
"copyFiles": {
|
||||
"label": "複製檔案",
|
||||
"helper": "要從原始儲存庫目錄複製到工作樹的檔案清單(以逗號分隔)。適合用於 .env 等環境檔案。請確保這些檔案已加入 gitignore!"
|
||||
"helper": "要從原始儲存庫目錄複製到工作樹的檔案清單(以逗號分隔)。適合用於 .env 等環境檔案。請確保這些檔案已加入 gitignore!",
|
||||
"placeholder": "檔案路徑或 glob 模式(例如:.env、config/*.json)"
|
||||
},
|
||||
"devServer": {
|
||||
"label": "開發伺服器腳本",
|
||||
|
||||
@@ -130,9 +130,10 @@
|
||||
"noDevScriptHint": "在專案設定中新增開發指令碼以啟用預覽。",
|
||||
"browser": {
|
||||
"title": "開發伺服器預覽",
|
||||
"startButton": "啟動開發伺服器",
|
||||
"stopButton": "停止",
|
||||
"startingButton": "啟動"
|
||||
"devServerFallback": "開發伺服器"
|
||||
},
|
||||
"urlInput": {
|
||||
"placeholder": "輸入 URL..."
|
||||
},
|
||||
"iframe": {
|
||||
"title": "開發伺服器預覽"
|
||||
|
||||
@@ -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<SearchResult[]> => {
|
||||
const modeParam = mode ? `&mode=${encodeURIComponent(mode)}` : '';
|
||||
@@ -895,6 +896,20 @@ export const repoApi = {
|
||||
});
|
||||
return handleApiResponse<OpenEditorResponse>(response);
|
||||
},
|
||||
|
||||
searchFiles: async (
|
||||
repoId: string,
|
||||
query: string,
|
||||
mode?: SearchMode,
|
||||
options?: RequestInit
|
||||
): Promise<SearchResult[]> => {
|
||||
const modeParam = mode ? `&mode=${encodeURIComponent(mode)}` : '';
|
||||
const response = await makeRequest(
|
||||
`/api/repos/${repoId}/search?q=${encodeURIComponent(query)}${modeParam}`,
|
||||
options
|
||||
);
|
||||
return handleApiResponse<SearchResult[]>(response);
|
||||
},
|
||||
};
|
||||
|
||||
// Config APIs (backwards compatible)
|
||||
|
||||
@@ -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() {
|
||||
<Label htmlFor="copy-files">
|
||||
{t('settings.repos.scripts.copyFiles.label')}
|
||||
</Label>
|
||||
<AutoExpandingTextarea
|
||||
id="copy-files"
|
||||
<MultiFileSearchTextarea
|
||||
value={draft.copy_files}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
|
||||
@@ -370,6 +370,8 @@ export type DirectoryEntry = { name: string, path: string, is_directory: boolean
|
||||
|
||||
export type DirectoryListResponse = { entries: Array<DirectoryEntry>, 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, };
|
||||
|
||||
Reference in New Issue
Block a user