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:
Gabriel Gordon-Hall
2026-01-13 17:34:54 +00:00
committed by GitHub
parent 5502a4cad6
commit cdfb081cf8
32 changed files with 296 additions and 241 deletions

View File

@@ -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}
/>
);
}

View File

@@ -75,7 +75,8 @@ export function PreviewControls({
)}
onClick={() => onTabChange(process.id)}
>
{getDevServerWorkingDir(process) ?? 'Dev Server'}
{getDevServerWorkingDir(process) ??
t('preview.browser.devServerFallback')}
</button>
))}
</div>

View File

@@ -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) => {

View File

@@ -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}}"
}

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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}}"
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -55,6 +55,5 @@
"projectNotFound": "お探しのプロジェクトは存在しないか、削除されました。",
"viewProject": "プロジェクトを表示",
"openInIDE": "IDEで開く",
"createdDate": "作成日 {{date}}",
"copyFilesPlaceholderWithSearch": "パスまたはglobパターンを入力 (.env, config/*.json)"
"createdDate": "作成日 {{date}}"
}

View File

@@ -380,7 +380,8 @@
},
"copyFiles": {
"label": "ファイルをコピー",
"helper": "元のリポジトリディレクトリからワークツリーにコピーするファイルのカンマ区切りリスト。.envなどの環境ファイルに役立ちます。gitignoreされていることを確認してください"
"helper": "元のリポジトリディレクトリからワークツリーにコピーするファイルのカンマ区切りリスト。.envなどの環境ファイルに役立ちます。gitignoreされていることを確認してください",
"placeholder": "ファイルパスまたはglobパターン.env、config/*.json"
},
"devServer": {
"label": "開発サーバースクリプト",

View File

@@ -371,9 +371,10 @@
},
"browser": {
"title": "開発サーバープレビュー",
"startButton": "開発サーバーを開始",
"stopButton": "停止",
"startingButton": "開始"
"devServerFallback": "開発サーバー"
},
"urlInput": {
"placeholder": "URLを入力..."
},
"noDevScript": "開発スクリプトが設定されていません",
"noDevScriptHint": "プレビューを有効にするには、プロジェクト設定で開発スクリプトを追加してください。",

View File

@@ -55,6 +55,5 @@
"projectNotFound": "찾으시는 프로젝트가 존재하지 않거나 삭제되었습니다.",
"viewProject": "프로젝트 보기",
"openInIDE": "IDE에서 열기",
"createdDate": "생성일 {{date}}",
"copyFilesPlaceholderWithSearch": "경로 또는 glob 패턴 입력 (.env, config/*.json)"
"createdDate": "생성일 {{date}}"
}

View File

@@ -380,7 +380,8 @@
},
"copyFiles": {
"label": "파일 복사",
"helper": "원래 저장소 디렉토리에서 워크트리로 복사할 파일의 쉼표로 구분된 목록입니다. .env와 같은 환경 파일에 유용합니다. gitignore되었는지 확인하세요!"
"helper": "원래 저장소 디렉토리에서 워크트리로 복사할 파일의 쉼표로 구분된 목록입니다. .env와 같은 환경 파일에 유용합니다. gitignore되었는지 확인하세요!",
"placeholder": "파일 경로 또는 glob 패턴 (예: .env, config/*.json)"
},
"devServer": {
"label": "개발 서버 스크립트",

View File

@@ -413,9 +413,10 @@
},
"browser": {
"title": "개발 서버 미리보기",
"startButton": "개발 서버 시작",
"stopButton": "중지",
"startingButton": "시작"
"devServerFallback": "개발 서버"
},
"urlInput": {
"placeholder": "URL 입력..."
},
"noDevScript": "개발 스크립트가 설정되지 않았습니다",
"noDevScriptHint": "미리보기를 활성화하려면 프로젝트 설정에서 개발 스크립트를 추가하세요."

View File

@@ -55,6 +55,5 @@
"projectNotFound": "您查找的项目不存在或已被删除。",
"viewProject": "查看项目",
"openInIDE": "在 IDE 中打开",
"createdDate": "创建于 {{date}}",
"copyFilesPlaceholderWithSearch": "文件路径或 glob 模式(例如:.env、config/*.json"
"createdDate": "创建于 {{date}}"
}

View File

@@ -380,7 +380,8 @@
},
"copyFiles": {
"label": "复制文件",
"helper": "要从原始仓库目录复制到工作树的文件的逗号分隔列表。对 .env 等环境文件很有用。确保这些文件被 gitignore"
"helper": "要从原始仓库目录复制到工作树的文件的逗号分隔列表。对 .env 等环境文件很有用。确保这些文件被 gitignore",
"placeholder": "文件路径或 glob 模式(例如:.env、config/*.json"
},
"devServer": {
"label": "开发服务器脚本",

View File

@@ -130,9 +130,10 @@
"noDevScriptHint": "在项目设置中添加开发脚本以启用预览。",
"browser": {
"title": "开发服务器预览",
"startButton": "启动开发服务器",
"stopButton": "停止",
"startingButton": "启动"
"devServerFallback": "开发服务器"
},
"urlInput": {
"placeholder": "输入 URL..."
},
"iframe": {
"title": "开发服务器预览"

View File

@@ -55,6 +55,5 @@
"projectNotFound": "您要查找的專案不存在或已被刪除。",
"viewProject": "查看專案",
"openInIDE": "在 IDE 中開啟",
"createdDate": "建立於 {{date}}",
"copyFilesPlaceholderWithSearch": "檔案路徑或 glob 模式(例如:.env、config/*.json"
"createdDate": "建立於 {{date}}"
}

View File

@@ -380,7 +380,8 @@
},
"copyFiles": {
"label": "複製檔案",
"helper": "要從原始儲存庫目錄複製到工作樹的檔案清單(以逗號分隔)。適合用於 .env 等環境檔案。請確保這些檔案已加入 gitignore"
"helper": "要從原始儲存庫目錄複製到工作樹的檔案清單(以逗號分隔)。適合用於 .env 等環境檔案。請確保這些檔案已加入 gitignore",
"placeholder": "檔案路徑或 glob 模式(例如:.env、config/*.json"
},
"devServer": {
"label": "開發伺服器腳本",

View File

@@ -130,9 +130,10 @@
"noDevScriptHint": "在專案設定中新增開發指令碼以啟用預覽。",
"browser": {
"title": "開發伺服器預覽",
"startButton": "啟動開發伺服器",
"stopButton": "停止",
"startingButton": "啟動"
"devServerFallback": "開發伺服器"
},
"urlInput": {
"placeholder": "輸入 URL..."
},
"iframe": {
"title": "開發伺服器預覽"

View File

@@ -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)

View File

@@ -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">