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:
@@ -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' && (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,6 @@
|
||||
"unlinkFromOrganization": "リモートプロジェクトからリンク解除",
|
||||
"viewProject": "プロジェクトを表示",
|
||||
"openInIDE": "IDEで開く",
|
||||
"createdDate": "作成日 {{date}}"
|
||||
"createdDate": "作成日 {{date}}",
|
||||
"copyFilesPlaceholderWithSearch": "パスまたはglobパターンを入力 (.env, config/*.json)"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,6 @@
|
||||
"unlinkFromOrganization": "원격 프로젝트에서 연결 해제",
|
||||
"viewProject": "프로젝트 보기",
|
||||
"openInIDE": "IDE에서 열기",
|
||||
"createdDate": "생성일 {{date}}"
|
||||
"createdDate": "생성일 {{date}}",
|
||||
"copyFilesPlaceholderWithSearch": "경로 또는 glob 패턴 입력 (.env, config/*.json)"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user