Glob copy files (#1420)

* Simplify glob file search

Switch to globwalk

* Cleanup

* Fix single file continue, add timeout to copy files

* Remove error in copy file dropdown when no match found

* Remove error message for file search text are, remove dead code

* Move copy logic to copy.rs
This commit is contained in:
Alex Netsch
2025-12-04 15:36:14 +00:00
committed by GitHub
parent 9bfaa6dde5
commit ef1ba1b4bb
13 changed files with 380 additions and 241 deletions

View File

@@ -39,16 +39,6 @@ const ProjectFormDialogImpl = NiceModal.create<ProjectFormDialogProps>(() => {
},
});
// Auto-populate project name from directory name
const handleGitRepoPathChange = (path: string) => {
setGitRepoPath(path);
if (path) {
const cleanName = generateProjectNameFromPath(path);
if (cleanName) setName(cleanName);
}
};
// Handle direct project creation from repo selection
const handleDirectCreate = async (path: string, suggestedName: string) => {
setError('');
@@ -125,27 +115,15 @@ const ProjectFormDialogImpl = NiceModal.create<ProjectFormDialogProps>(() => {
<div className="mx-auto w-full max-w-2xl overflow-x-hidden px-1">
<form onSubmit={handleSubmit} className="space-y-4">
<ProjectFormFields
isEditing={false}
repoMode={repoMode}
setRepoMode={setRepoMode}
gitRepoPath={gitRepoPath}
handleGitRepoPathChange={handleGitRepoPathChange}
parentPath={parentPath}
setParentPath={setParentPath}
setFolderName={setFolderName}
setName={setName}
name={name}
setupScript=""
setSetupScript={() => {}}
devScript=""
setDevScript={() => {}}
cleanupScript=""
setCleanupScript={() => {}}
copyFiles=""
setCopyFiles={() => {}}
error={error}
setError={setError}
projectId={undefined}
onCreateProject={handleDirectCreate}
/>
{repoMode === 'new' && (

View File

@@ -1,9 +1,10 @@
import { useTranslation } from 'react-i18next';
import { MultiFileSearchTextarea } from '@/components/ui/multi-file-search-textarea';
interface CopyFilesFieldProps {
value: string;
onChange: (value: string) => void;
projectId?: string;
projectId: string;
disabled?: boolean;
}
@@ -13,31 +14,18 @@ export function CopyFilesField({
projectId,
disabled = false,
}: CopyFilesFieldProps) {
if (projectId) {
// Editing existing project - use file search
return (
<MultiFileSearchTextarea
value={value}
onChange={onChange}
placeholder="Start typing a file path... (.env, config.local.json, .local/settings.yml)"
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}
/>
);
}
const { t } = useTranslation('projects');
// Creating new project - fall back to plain textarea
return (
<textarea
<MultiFileSearchTextarea
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder=".env,config.local.json,.local/settings.yml"
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 rounded-md resize-vertical focus:outline-none focus:ring-2 focus:ring-ring"
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

@@ -11,8 +11,6 @@ import {
FolderPlus,
ArrowLeft,
} from 'lucide-react';
import { useScriptPlaceholders } from '@/hooks/useScriptPlaceholders';
import { CopyFilesField } from './CopyFilesField';
// Removed collapsible sections for simplicity; show fields always in edit mode
import { fileSystemApi } from '@/lib/api';
import { FolderPickerDialog } from '@/components/dialogs/shared/FolderPickerDialog';
@@ -20,56 +18,30 @@ import { DirectoryEntry } from 'shared/types';
import { generateProjectNameFromPath } from '@/utils/string';
interface ProjectFormFieldsProps {
isEditing: boolean;
repoMode: 'existing' | 'new';
setRepoMode: (mode: 'existing' | 'new') => void;
gitRepoPath: string;
handleGitRepoPathChange: (path: string) => void;
parentPath: string;
setParentPath: (path: string) => void;
setFolderName: (name: string) => void;
setName: (name: string) => void;
name: string;
setupScript: string;
setSetupScript: (script: string) => void;
devScript: string;
setDevScript: (script: string) => void;
cleanupScript: string;
setCleanupScript: (script: string) => void;
copyFiles: string;
setCopyFiles: (files: string) => void;
error: string;
setError: (error: string) => void;
projectId?: string;
onCreateProject?: (path: string, name: string) => void;
}
export function ProjectFormFields({
isEditing,
repoMode,
setRepoMode,
gitRepoPath,
handleGitRepoPathChange,
parentPath,
setParentPath,
setFolderName,
setName,
name,
setupScript,
setSetupScript,
devScript,
setDevScript,
cleanupScript,
setCleanupScript,
copyFiles,
setCopyFiles,
error,
setError,
projectId,
onCreateProject,
}: ProjectFormFieldsProps) {
const placeholders = useScriptPlaceholders();
// Repository loading state
const [allRepos, setAllRepos] = useState<DirectoryEntry[]>([]);
const [loading, setLoading] = useState(false);
@@ -94,14 +66,14 @@ export function ProjectFormFields({
// Lazy-load repositories when the user navigates to the repo list
useEffect(() => {
if (!isEditing && showRecentRepos && !loading && allRepos.length === 0) {
if (showRecentRepos && !loading && allRepos.length === 0) {
loadRecentRepos();
}
}, [isEditing, showRecentRepos, loading, allRepos.length, loadRecentRepos]);
}, [showRecentRepos, loading, allRepos.length, loadRecentRepos]);
return (
<>
{!isEditing && repoMode === 'existing' && (
{repoMode === 'existing' && (
<div className="space-y-4">
{/* Show selection interface only when no repo is selected */}
<>
@@ -276,7 +248,7 @@ export function ProjectFormFields({
)}
{/* Blank Project Form */}
{!isEditing && repoMode === 'new' && (
{repoMode === 'new' && (
<div className="space-y-4">
{/* Back button */}
<Button
@@ -362,127 +334,6 @@ export function ProjectFormFields({
</div>
</div>
)}
{isEditing && (
<>
<div className="space-y-2">
<Label htmlFor="git-repo-path">Git Repository Path</Label>
<div className="flex space-x-2">
<Input
id="git-repo-path"
type="text"
value={gitRepoPath}
onChange={(e) => handleGitRepoPathChange(e.target.value)}
placeholder="/path/to/your/existing/repo"
required
className="flex-1"
/>
<Button
type="button"
variant="outline"
onClick={async () => {
const selectedPath = await FolderPickerDialog.show({
title: 'Select Git Repository',
description: 'Choose an existing git repository',
value: gitRepoPath,
});
if (selectedPath) {
handleGitRepoPathChange(selectedPath);
}
}}
>
<Folder className="h-4 w-4" />
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="name">Project Name</Label>
<Input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter project name"
required
/>
</div>
</>
)}
{isEditing && (
<div className="space-y-4 pt-4 border-t border-border">
<div className="space-y-2">
<Label htmlFor="setup-script">Setup Script</Label>
<textarea
id="setup-script"
value={setupScript}
onChange={(e) => setSetupScript(e.target.value)}
placeholder={placeholders.setup}
rows={4}
className="w-full px-3 py-2 text-sm border border-input bg-background text-foreground rounded-md resize-vertical focus:outline-none focus:ring-2 focus:ring-ring"
/>
<p className="text-sm text-muted-foreground">
This script will run after creating the worktree and before the
coding agent starts. Use it for setup tasks like installing
dependencies or preparing the environment.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="dev-script">Dev Server Script</Label>
<textarea
id="dev-script"
value={devScript}
onChange={(e) => setDevScript(e.target.value)}
placeholder={placeholders.dev}
rows={4}
className="w-full px-3 py-2 text-sm border border-input bg-background text-foreground rounded-md resize-vertical focus:outline-none focus:ring-2 focus:ring-ring"
/>
<p className="text-sm text-muted-foreground">
This script can be run from task attempts to start a development
server. Use it to quickly start your project's dev server for
testing changes.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="cleanup-script">Cleanup Script</Label>
<textarea
id="cleanup-script"
value={cleanupScript}
onChange={(e) => setCleanupScript(e.target.value)}
placeholder={placeholders.cleanup}
rows={4}
className="w-full px-3 py-2 text-sm border border-input bg-background text-foreground rounded-md resize-vertical focus:outline-none focus:ring-2 focus:ring-ring"
/>
<p className="text-sm text-muted-foreground">
This script runs after coding agent execution{' '}
<strong>only if changes were made</strong>. Use it for quality
assurance tasks like running linters, formatters, tests, or other
validation steps. If no changes are made, this script is skipped.
</p>
</div>
<div className="space-y-2">
<Label>Copy Files</Label>
<CopyFilesField
value={copyFiles}
onChange={setCopyFiles}
projectId={projectId}
/>
<p className="text-sm text-muted-foreground">
Comma-separated list of files to copy from the original project
directory to the worktree. These files will be copied after the
worktree is created but before the setup script runs. Useful for
environment-specific files like .env, configuration files, and
local settings. Make sure these are gitignored or they could get
committed!
</p>
</div>
</div>
)}
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />

View File

@@ -58,7 +58,7 @@ export function MultiFileSearchTextarea({
const cached = searchCacheRef.current.get(searchQuery);
if (cached) {
setSearchResults(cached);
setShowDropdown(true);
setShowDropdown(cached.length > 0);
setSelectedIndex(-1);
return;
}
@@ -95,7 +95,7 @@ export function MultiFileSearchTextarea({
searchCacheRef.current.set(searchQuery, fileResults);
setSearchResults(fileResults);
setShowDropdown(true);
setShowDropdown(fileResults.length > 0);
setSelectedIndex(-1);
}
} catch (error) {
@@ -354,10 +354,6 @@ export function MultiFileSearchTextarea({
<div className="p-2 text-sm text-muted-foreground">
Searching...
</div>
) : searchResults.length === 0 ? (
<div className="p-2 text-sm text-muted-foreground">
No files found
</div>
) : (
<div className="py-1">
{searchResults.map((file, index) => (

View File

@@ -46,5 +46,6 @@
"unlinkFromOrganization": "Unlink from Remote Project",
"viewProject": "View Project",
"openInIDE": "Open in IDE",
"createdDate": "Created {{date}}"
"createdDate": "Created {{date}}",
"copyFilesPlaceholderWithSearch": "File paths or glob patterns (e.g., .env, config/*.json)"
}

View File

@@ -46,5 +46,6 @@
"unlinkFromOrganization": "Desvincular de Proyecto Remoto",
"viewProject": "Ver Proyecto",
"openInIDE": "Abrir en IDE",
"createdDate": "Creado {{date}}"
"createdDate": "Creado {{date}}",
"copyFilesPlaceholderWithSearch": "Escribe una ruta o patrón glob (.env, config/*.json)"
}

View File

@@ -46,5 +46,6 @@
"unlinkFromOrganization": "リモートプロジェクトからリンク解除",
"viewProject": "プロジェクトを表示",
"openInIDE": "IDEで開く",
"createdDate": "作成日 {{date}}"
"createdDate": "作成日 {{date}}",
"copyFilesPlaceholderWithSearch": "パスまたはglobパターンを入力 (.env, config/*.json)"
}

View File

@@ -46,5 +46,6 @@
"unlinkFromOrganization": "원격 프로젝트에서 연결 해제",
"viewProject": "프로젝트 보기",
"openInIDE": "IDE에서 열기",
"createdDate": "생성일 {{date}}"
"createdDate": "생성일 {{date}}",
"copyFilesPlaceholderWithSearch": "경로 또는 glob 패턴 입력 (.env, config/*.json)"
}