Easier project creation (#600)

* Easier project creation (vibe-kanban 71f2ce0b)

The current project creation screen is complicated and without any good defaults. We need to make it easier to create projects, offering existing git repos as base.

Easier project creation (vibe-kanban 71f2ce0b)

The current project creation screen is complicated and without any good defaults. We need to make it easier to create projects, offering existing git repos as base.

Easier project creation (vibe-kanban 71f2ce0b)

The current project creation screen is complicated and without any good defaults. We need to make it easier to create projects, offering existing git repos as base.

Better project creation menu (vibe-kanban 0f35d0be)

WHen creating a project from an existing repo, maybe instead of the show more with arrow in the middle we could move that to the right and have a "+ Find" sort of button to the left of it? that would then open the other thing?

Better project creation menu (vibe-kanban 0f35d0be)

WHen creating a project from an existing repo, maybe instead of the show more with arrow in the middle we could move that to the right and have a "+ Find" sort of button to the left of it? that would then open the other thing?

Better project creation menu (vibe-kanban 0f35d0be)

WHen creating a project from an existing repo, maybe instead of the show more with arrow in the middle we could move that to the right and have a "+ Find" sort of button to the left of it? that would then open the other thing?

Fix branch icon (vibe-kanban 59e0ee6e)

We added some stuff to make project creation easier in the last few commits, but now the branch icon for the selected branch goes invisible when selecting one. ![Screenshot 2025-09-01 at 15.55.57.png](.vibe-images/94bc9ba5-b6a8-4ea3-bfb1-60adeed966d7.png)

Fix branch icon (vibe-kanban 59e0ee6e)

We added some stuff to make project creation easier in the last few commits, but now the branch icon for the selected branch goes invisible when selecting one. ![Screenshot 2025-09-01 at 15.55.57.png](.vibe-images/94bc9ba5-b6a8-4ea3-bfb1-60adeed966d7.png)

Fix branch icon (vibe-kanban 59e0ee6e)

We added some stuff to make project creation easier in the last few commits, but now the branch icon for the selected branch goes invisible when selecting one. ![Screenshot 2025-09-01 at 15.55.57.png](.vibe-images/94bc9ba5-b6a8-4ea3-bfb1-60adeed966d7.png)

Fix branch icon (vibe-kanban 59e0ee6e)

We added some stuff to make project creation easier in the last few commits, but now the branch icon for the selected branch goes invisible when selecting one. ![Screenshot 2025-09-01 at 15.55.57.png](.vibe-images/94bc9ba5-b6a8-4ea3-bfb1-60adeed966d7.png)

Project creation layout (vibe-kanban 4fbf61c8)

atch the \"New Repository\" structure:\n```tsx\n{/* Display selected repository fields */}\n{selectedPath && selectedRepo && (\n  <div className=\"space-y-4\">\n    <div className=\"space-y-2\">\n      <Label htmlFor=\"base-dir\">Base Directory</Label>\n      <div className=\"flex space-x-2\">\n        <Input\n          id=\"base-dir\"\n          type=\"text\"\n          value={selectedRepo.path.substring(0, selectedRepo.path.lastIndexOf('/'))}\n          disabled\n          readOnly\n          className=\"flex-1\"\n        />\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon\"\n          disabled\n          className=\"opacity-50\"\n        >\n          <Folder className=\"h-4 w-4\" />\n        </Button>\n      </div>\n    </div>\n\n    <div className=\"space-y-2\">\n      <Label htmlFor=\"repo-name\">Repository Folder Name</Label>\n      <Input\n        id=\"repo-name\"\n        type=\"text\"\n        value={selectedRepo.name}\n        disabled\n        readOnly\n      />\n    </div>\n  </div>\n)}\n```\n\n### Visual Result\n\n**Existing Repository Mode:**\n```\n[Repository Dropdown ▼] [🔍]\n\nBase Directory\n[/path/to/parent] [📁] (disabled)\n\nRepository Folder Name\n[repo-name] (disabled)\n```\n\n**New Repository Mode:**\n```\nParent Directory\n[/path/to/parent] [📁] (editable)\n\nRepository Folder Name\n[repo-name] (editable)\n```\n\n### Benefits\n\n1. **Visual Consistency**: Both modes show the same fields in the same layout\n2. **Clear Distinction**: Disabled/readonly fields for existing repos vs editable for new\n3. **Better Information**: Users can see the base directory and folder name clearly\n4. **Cleaner Design**: No need for special card styling, uses standard form fields\n5. **Intuitive**: The disabled state clearly indicates these are display-only for existing repos\n\n### Technical Notes\n\n- Use `disabled` and `readOnly` props on Input components for existing repos\n- Extract base directory using `substring` and `lastIndexOf('/')`\n- Keep the folder button disabled with `opacity-50` for visual consistency\n- Remove the help text for existing repos since fields are self-explanatory when read-only

Change tabs to align with edit (vibe-kanban 7b589225)

For project creation, the tabs for From git and blank project should have the same appearance as the tabs in edit project

Cleanup changes (vibe-kanban e498187d)

Cleanup the changes made in the last four commits. The changes are good, but there may be unsused things that didnt get cleaned up

Review changes (vibe-kanban 9a859f73)

Make sure the stuff add in the last 6 commits reuses components instead of duplicating, among others look at the tabs/collapsible stuff

Cleanup changes (vibe-kanban e498187d)

Cleanup the changes made in the last commit. The changes are good, but there may be unsused things that didnt get cleaned up, there may be things we remove that shouldve stayed, like the tabnavigation

Project creation submission (vibe-kanban e8fcfd73)

When collapsing things while creating a project, it submits the form instead of collapsing the section

fmt, cleanup

Default parent path (vibe-kanban 9be78842)

For project creation, when creating a blank vk project, we should have a deafault parent path or make it more lear the user has to select one

Default parent path (vibe-kanban 9be78842)

For project creation, when creating a blank vk project, we should have a deafault parent path or make it more lear the user has to select one

Default parent path (vibe-kanban 9be78842)

For project creation, when creating a blank vk project, we should have a deafault parent path or make it more lear the user has to select one

Default parent path (vibe-kanban 9be78842)

For project creation, when creating a blank vk project, we should have a deafault parent path or make it more lear the user has to select one

Default parent path (vibe-kanban 9be78842)

For project creation, when creating a blank vk project, we should have a deafault parent path or make it more lear the user has to select one

Default parent path (vibe-kanban 9be78842)

For project creation, when creating a blank vk project, we should have a deafault parent path or make it more lear the user has to select one

* Update Rust edition to 2024 and refactor project routes for improved clarity

fmt

* Project creation layout (vibe-kanban f726d2e6)

When creating a new project, users should see only the repo selection at first, after selecting one the rest of the options appears.

Remove script options from project creation screen (vibe-kanban 049226af)

Project creation does not need to show script options, these should only be available via edit project.

Better add project (vibe-kanban 79e936bc)

When no projects are available, we should display project creation options right away

Project creation style (vibe-kanban 91bce79b)

The styling of the project creation dialog should be unified with the rest of the project

Review (vibe-kanban 4f8f8068)

Review this PR: https://github.com/BloopAI/vibe-kanban/pull/600
The github cli should work.

Just review, no changes!

fmt

Fix changes lost in rebase

fmt

remove unused collapsible section, remove duplicate default repo path

Re-add detailed script descriptions
This commit is contained in:
Alex Netsch
2025-09-03 09:27:50 +01:00
committed by GitHub
parent b9a1a9f33c
commit 5453d70b2e
23 changed files with 597 additions and 334 deletions

View File

@@ -1,14 +1,26 @@
import { useState, useEffect } from 'react';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { AlertCircle, Folder } from 'lucide-react';
import {
AlertCircle,
Folder,
Search,
FolderGit,
FolderPlus,
ArrowLeft,
} from 'lucide-react';
import {
createScriptPlaceholderStrategy,
ScriptPlaceholderContext,
} from '@/utils/script-placeholders';
import { useUserSystem } from '@/components/config-provider';
import { CopyFilesField } from './copy-files-field';
// Removed collapsible sections for simplicity; show fields always in edit mode
import { fileSystemApi } from '@/lib/api';
import { DirectoryEntry } from 'shared/types';
import { generateProjectNameFromPath } from '@/utils/string';
interface ProjectFormFieldsProps {
isEditing: boolean;
@@ -19,7 +31,6 @@ interface ProjectFormFieldsProps {
setShowFolderPicker: (show: boolean) => void;
parentPath: string;
setParentPath: (path: string) => void;
folderName: string;
setFolderName: (name: string) => void;
setName: (name: string) => void;
name: string;
@@ -32,7 +43,9 @@ interface ProjectFormFieldsProps {
copyFiles: string;
setCopyFiles: (files: string) => void;
error: string;
setError: (error: string) => void;
projectId?: string;
onCreateProject?: (path: string, name: string) => void;
}
export function ProjectFormFields({
@@ -44,7 +57,6 @@ export function ProjectFormFields({
setShowFolderPicker,
parentPath,
setParentPath,
folderName,
setFolderName,
setName,
name,
@@ -57,7 +69,9 @@ export function ProjectFormFields({
copyFiles,
setCopyFiles,
error,
setError,
projectId,
onCreateProject,
}: ProjectFormFieldsProps) {
const { system } = useUserSystem();
@@ -73,80 +87,291 @@ export function ProjectFormFields({
'#!/bin/bash\n# Add cleanup commands here...\n# This runs after coding agent execution',
};
// Repository loading state
const [allRepos, setAllRepos] = useState<DirectoryEntry[]>([]);
const [loading, setLoading] = useState(false);
const [reposError, setReposError] = useState('');
const [showMoreOptions, setShowMoreOptions] = useState(false);
const [showRecentRepos, setShowRecentRepos] = useState(false);
// Lazy-load repositories when the user navigates to the repo list
useEffect(() => {
if (!isEditing && showRecentRepos && !loading && allRepos.length === 0) {
loadRecentRepos();
}
}, [isEditing, showRecentRepos]);
const loadRecentRepos = async () => {
setLoading(true);
setReposError('');
try {
const discoveredRepos = await fileSystemApi.listGitRepos();
setAllRepos(discoveredRepos);
} catch (err) {
setReposError('Failed to load repositories');
console.error('Failed to load repos:', err);
} finally {
setLoading(false);
}
};
return (
<>
{!isEditing && (
<div className="space-y-3">
<Label>Repository Type</Label>
<div className="flex space-x-4">
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="radio"
name="repoMode"
value="existing"
checked={repoMode === 'existing'}
onChange={(e) =>
setRepoMode(e.target.value as 'existing' | 'new')
}
className="text-primary"
{!isEditing && repoMode === 'existing' && (
<div className="space-y-4">
{/* Show selection interface only when no repo is selected */}
<>
{/* Initial choice cards - Stage 1 */}
{!showRecentRepos && (
<>
{/* From Git Repository card */}
<div
className="p-4 border cursor-pointer hover:shadow-md transition-shadow rounded-lg bg-card"
onClick={() => setShowRecentRepos(true)}
>
<div className="flex items-start gap-3">
<FolderGit className="h-5 w-5 mt-0.5 flex-shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<div className="font-medium text-foreground">
From Git Repository
</div>
<div className="text-xs text-muted-foreground mt-1">
Use an existing repository as your project base
</div>
</div>
</div>
</div>
{/* Create Blank Project card */}
<div
className="p-4 border cursor-pointer hover:shadow-md transition-shadow rounded-lg bg-card"
onClick={() => {
setRepoMode('new');
setError('');
}}
>
<div className="flex items-start gap-3">
<FolderPlus className="h-5 w-5 mt-0.5 flex-shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<div className="font-medium text-foreground">
Create Blank Project
</div>
<div className="text-xs text-muted-foreground mt-1">
Start a new project from scratch
</div>
</div>
</div>
</div>
</>
)}
{/* Repository selection - Stage 2A */}
{showRecentRepos && (
<>
{/* Back button */}
<button
className="text-sm text-muted-foreground hover:text-foreground flex items-center gap-1 mb-4"
onClick={() => {
setShowRecentRepos(false);
setError('');
}}
>
<ArrowLeft className="h-3 w-3" />
Back to options
</button>
{/* Repository cards */}
{!loading && allRepos.length > 0 && (
<div className="space-y-2">
{allRepos
.slice(0, showMoreOptions ? allRepos.length : 3)
.map((repo) => (
<div
key={repo.path}
className="p-4 border cursor-pointer hover:shadow-md transition-shadow rounded-lg bg-card"
onClick={() => {
setError('');
const cleanName = generateProjectNameFromPath(
repo.path
);
onCreateProject?.(repo.path, cleanName);
}}
>
<div className="flex items-start gap-3">
<FolderGit className="h-5 w-5 mt-0.5 flex-shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<div className="font-medium text-foreground">
{repo.name}
</div>
<div className="text-xs text-muted-foreground truncate mt-1">
{repo.path}
</div>
</div>
</div>
</div>
))}
{/* Show more/less for repositories */}
{!showMoreOptions && allRepos.length > 3 && (
<button
className="text-sm text-muted-foreground hover:text-foreground transition-colors text-left"
onClick={() => setShowMoreOptions(true)}
>
Show {allRepos.length - 3} more repositories
</button>
)}
{showMoreOptions && allRepos.length > 3 && (
<button
className="text-sm text-muted-foreground hover:text-foreground transition-colors text-left"
onClick={() => setShowMoreOptions(false)}
>
Show less
</button>
)}
</div>
)}
{/* Loading state */}
{loading && (
<div className="p-4 border rounded-lg bg-card">
<div className="flex items-center gap-3">
<div className="animate-spin h-5 w-5 border-2 border-muted-foreground border-t-transparent rounded-full"></div>
<div className="text-sm text-muted-foreground">
Loading repositories...
</div>
</div>
</div>
)}
{/* Error state */}
{!loading && reposError && (
<div className="p-4 border border-destructive rounded-lg bg-destructive/5">
<div className="flex items-center gap-3">
<AlertCircle className="h-5 w-5 text-destructive flex-shrink-0" />
<div className="text-sm text-destructive">
{reposError}
</div>
</div>
</div>
)}
{/* Browse for repository card */}
<div
className="p-4 border border-dashed cursor-pointer hover:shadow-md transition-shadow rounded-lg bg-card"
onClick={() => {
setShowFolderPicker(true);
setError('');
}}
>
<div className="flex items-start gap-3">
<Search className="h-5 w-5 mt-0.5 flex-shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<div className="font-medium text-foreground">
Search all repos
</div>
<div className="text-xs text-muted-foreground mt-1">
Browse and select any repository on your system
</div>
</div>
</div>
</div>
</>
)}
</>
</div>
)}
{/* Blank Project Form */}
{!isEditing && repoMode === 'new' && (
<div className="space-y-4">
{/* Back button */}
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setRepoMode('existing');
setError('');
setName('');
setParentPath('');
setFolderName('');
}}
className="flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Back to options
</Button>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="new-project-name">
Project Name <span className="text-red-500">*</span>
</Label>
<Input
id="new-project-name"
type="text"
value={name}
onChange={(e) => {
setName(e.target.value);
if (e.target.value) {
setFolderName(
e.target.value
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
);
}
}}
placeholder="My Awesome Project"
className="placeholder:text-secondary-foreground placeholder:opacity-100"
required
/>
<span className="text-sm">Use existing repository</span>
</label>
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="radio"
name="repoMode"
value="new"
checked={repoMode === 'new'}
onChange={(e) =>
setRepoMode(e.target.value as 'existing' | 'new')
}
className="text-primary"
/>
<span className="text-sm">Create new repository</span>
</label>
<p className="text-xs text-muted-foreground">
The folder name will be auto-generated from the project name
</p>
</div>
<div className="space-y-2">
<Label htmlFor="parent-path">Parent Directory</Label>
<div className="flex space-x-2">
<Input
id="parent-path"
type="text"
value={parentPath}
onChange={(e) => setParentPath(e.target.value)}
placeholder="Home"
className="flex-1 placeholder:text-secondary-foreground placeholder:opacity-100"
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setShowFolderPicker(true)}
>
<Folder className="h-4 w-4" />
</Button>
</div>
<p className="text-xs text-muted-foreground">
Leave empty to use your home directory, or specify a custom
path.
</p>
</div>
</div>
</div>
)}
{repoMode === 'existing' || 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={() => setShowFolderPicker(true)}
>
<Folder className="h-4 w-4" />
</Button>
</div>
{!isEditing && (
<p className="text-sm text-muted-foreground">
Select a folder that already contains a git repository
</p>
)}
</div>
) : (
<div className="space-y-4">
{isEditing && (
<>
<div className="space-y-2">
<Label htmlFor="parent-path">Parent Directory</Label>
<Label htmlFor="git-repo-path">Git Repository Path</Label>
<div className="flex space-x-2">
<Input
id="parent-path"
id="git-repo-path"
type="text"
value={parentPath}
onChange={(e) => setParentPath(e.target.value)}
placeholder="/path/to/parent/directory"
value={gitRepoPath}
onChange={(e) => handleGitRepoPathChange(e.target.value)}
placeholder="/path/to/your/existing/repo"
required
className="flex-1"
/>
@@ -158,118 +383,95 @@ export function ProjectFormFields({
<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">
Choose where to create the new repository
This script will run after creating the worktree and before the
executor starts. Use it for setup tasks like installing
dependencies or preparing the environment.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="folder-name">Repository Folder Name</Label>
<Input
id="folder-name"
type="text"
value={folderName}
onChange={(e) => {
setFolderName(e.target.value);
if (e.target.value) {
setName(
e.target.value
.replace(/[-_]/g, ' ')
.replace(/\b\w/g, (l) => l.toUpperCase())
);
}
}}
placeholder="my-awesome-project"
required
className="flex-1"
<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">
The project name will be auto-populated from this folder name
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>
)}
<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>
<div className="space-y-2">
<Label htmlFor="setup-script">Setup Script (Optional)</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
executor 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 (Optional)</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 (Optional)</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 htmlFor="copy-files">Copy Files (Optional)</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>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />

View File

@@ -14,6 +14,7 @@ import { TaskTemplateManager } from '@/components/TaskTemplateManager';
import { ProjectFormFields } from './project-form-fields';
import { CreateProject, Project, UpdateProject } from 'shared/types';
import { projectsApi } from '@/lib/api';
import { generateProjectNameFromPath } from '@/utils/string';
interface ProjectFormProps {
open: boolean;
@@ -42,6 +43,7 @@ export function ProjectForm({
const [repoMode, setRepoMode] = useState<'existing' | 'new'>('existing');
const [parentPath, setParentPath] = useState('');
const [folderName, setFolderName] = useState('');
// Removed manual repo preview flow; quick-create on selection instead
const isEditing = !!project;
@@ -70,15 +72,42 @@ export function ProjectForm({
// Only auto-populate name for new projects
if (!isEditing && path) {
// Extract the last part of the path (directory name)
const dirName = path.split('/').filter(Boolean).pop() || '';
if (dirName) {
// Clean up the directory name for a better project name
const cleanName = dirName
.replace(/[-_]/g, ' ') // Replace hyphens and underscores with spaces
.replace(/\b\w/g, (l) => l.toUpperCase()); // Capitalize first letter of each word
setName(cleanName);
}
const cleanName = generateProjectNameFromPath(path);
if (cleanName) setName(cleanName);
}
};
// Handle direct project creation from repo selection
const handleDirectCreate = async (path: string, suggestedName: string) => {
setError('');
setLoading(true);
try {
const createData: CreateProject = {
name: suggestedName,
git_repo_path: path,
use_existing_repo: true,
setup_script: null,
dev_script: null,
cleanup_script: null,
copy_files: null,
};
await projectsApi.create(createData);
onSuccess();
// Reset form
setName('');
setGitRepoPath('');
setSetupScript('');
setDevScript('');
setCleanupScript('');
setCopyFiles('');
setParentPath('');
setFolderName('');
} catch (error) {
setError(error instanceof Error ? error.message : 'An error occurred');
} finally {
setLoading(false);
}
};
@@ -88,15 +117,22 @@ export function ProjectForm({
setLoading(true);
try {
if (isEditing) {
// Editing existing project (local mode only)
let finalGitRepoPath = gitRepoPath;
if (repoMode === 'new') {
finalGitRepoPath = `${parentPath}/${folderName}`.replace(/\/+/g, '/');
}
let finalGitRepoPath = gitRepoPath;
if (repoMode === 'new') {
// Use home directory (~) if parentPath is empty
const effectiveParentPath = parentPath.trim() || '~';
finalGitRepoPath = `${effectiveParentPath}/${folderName}`.replace(
/\/+/g,
'/'
);
}
// Auto-populate name from git repo path if not provided
const finalName =
name.trim() || generateProjectNameFromPath(finalGitRepoPath);
if (isEditing) {
const updateData: UpdateProject = {
name,
name: finalName,
git_repo_path: finalGitRepoPath,
setup_script: setupScript.trim() || null,
dev_script: devScript.trim() || null,
@@ -126,20 +162,14 @@ export function ProjectForm({
// await githubApi.createProjectFromRepository(githubData);
// } else {
// Local mode: Create local project
let finalGitRepoPath = gitRepoPath;
if (repoMode === 'new') {
finalGitRepoPath = `${parentPath}/${folderName}`.replace(/\/+/g, '/');
}
const createData: CreateProject = {
name,
name: finalName,
git_repo_path: finalGitRepoPath,
use_existing_repo: repoMode === 'existing',
setup_script: setupScript.trim() || null,
dev_script: devScript.trim() || null,
cleanup_script: cleanupScript.trim() || null,
copy_files: copyFiles.trim() || null,
setup_script: null,
dev_script: null,
cleanup_script: null,
copy_files: null,
};
await projectsApi.create(createData);
@@ -185,17 +215,15 @@ export function ProjectForm({
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent
className={isEditing ? 'sm:max-w-[600px]' : 'sm:max-w-[425px]'}
>
<DialogContent>
<DialogHeader>
<DialogTitle>
{isEditing ? 'Edit Project' : 'Create New Project'}
{isEditing ? 'Edit Project' : 'Create Project'}
</DialogTitle>
<DialogDescription>
{isEditing
? "Make changes to your project here. Click save when you're done."
: 'Choose whether to use an existing git repository or create a new one.'}
: 'Choose your repository source'}
</DialogDescription>
</DialogHeader>
@@ -216,7 +244,6 @@ export function ProjectForm({
setShowFolderPicker={setShowFolderPicker}
parentPath={parentPath}
setParentPath={setParentPath}
folderName={folderName}
setFolderName={setFolderName}
setName={setName}
name={name}
@@ -229,20 +256,13 @@ export function ProjectForm({
copyFiles={copyFiles}
setCopyFiles={setCopyFiles}
error={error}
projectId={(project as any)?.id}
setError={setError}
projectId={project ? project.id : undefined}
/>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={handleClose}
disabled={loading}
>
Cancel
</Button>
<Button
type="submit"
disabled={loading || !name.trim() || !gitRepoPath.trim()}
disabled={loading || !gitRepoPath.trim()}
>
{loading ? 'Saving...' : 'Save Changes'}
</Button>
@@ -250,7 +270,9 @@ export function ProjectForm({
</form>
</TabsContent>
<TabsContent value="templates" className="mt-0 pt-0">
<TaskTemplateManager projectId={project?.id} />
<TaskTemplateManager
projectId={project ? project.id : undefined}
/>
</TabsContent>
</Tabs>
) : (
@@ -327,7 +349,6 @@ export function ProjectForm({
setShowFolderPicker={setShowFolderPicker}
parentPath={parentPath}
setParentPath={setParentPath}
folderName={folderName}
setFolderName={setFolderName}
setName={setName}
name={name}
@@ -340,31 +361,18 @@ export function ProjectForm({
copyFiles={copyFiles}
setCopyFiles={setCopyFiles}
error={error}
projectId={(project as any)?.id}
setError={setError}
projectId={(project as Project | null | undefined)?.id}
onCreateProject={handleDirectCreate}
/>
{/* )} */}
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={handleClose}
disabled={loading}
>
Cancel
</Button>
<Button
type="submit"
disabled={
loading ||
!name.trim() ||
(repoMode === 'existing'
? !gitRepoPath.trim()
: !parentPath.trim() || !folderName.trim())
}
>
{loading ? 'Creating...' : 'Create Project'}
</Button>
</DialogFooter>
{repoMode === 'new' && (
<DialogFooter>
<Button type="submit" disabled={loading || !folderName.trim()}>
{loading ? 'Creating...' : 'Create Project'}
</Button>
</DialogFooter>
)}
</form>
)}
</DialogContent>
@@ -374,7 +382,14 @@ export function ProjectForm({
onClose={() => setShowFolderPicker(false)}
onSelect={(path) => {
if (repoMode === 'existing' || isEditing) {
handleGitRepoPathChange(path);
if (isEditing) {
// For editing, just set the path
handleGitRepoPathChange(path);
} else {
// For creating, immediately attempt to create project (same as quick select)
const projectName = generateProjectNameFromPath(path);
handleDirectCreate(path, projectName);
}
} else {
setParentPath(path);
}

View File

@@ -13,6 +13,7 @@ import {
DeviceFlowStartResponse,
DevicePollStatus,
DirectoryListResponse,
DirectoryEntry,
EditorType,
ExecutionProcess,
GitBranch,
@@ -459,6 +460,14 @@ export const fileSystemApi = {
);
return handleApiResponse<DirectoryListResponse>(response);
},
listGitRepos: async (path?: string): Promise<DirectoryEntry[]> => {
const queryParam = path ? `?path=${encodeURIComponent(path)}` : '';
const response = await makeRequest(
`/api/filesystem/git-repos${queryParam}`
);
return handleApiResponse<DirectoryEntry[]>(response);
},
};
// Config APIs (backwards compatible)

View File

@@ -9,3 +9,14 @@ export const toPrettyCase = (value: string): string => {
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
};
/**
* Generates a pretty project name from a file path
* Converts directory names like "my-awesome-project" to "My Awesome Project"
* @param path - The file path to extract name from
* @returns Formatted project name
*/
export const generateProjectNameFromPath = (path: string): string => {
const dirName = path.split('/').filter(Boolean).pop() || '';
return dirName.replace(/[-_]/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
};