diff --git a/src/components/ProjectCreationWizard.jsx b/src/components/ProjectCreationWizard.jsx
deleted file mode 100644
index a41799ce..00000000
--- a/src/components/ProjectCreationWizard.jsx
+++ /dev/null
@@ -1,874 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { X, FolderPlus, GitBranch, Key, ChevronRight, ChevronLeft, Check, Loader2, AlertCircle, FolderOpen, Eye, EyeOff, Plus } from 'lucide-react';
-import { Button, Input } from '../shared/view/ui';
-import { api } from '../utils/api';
-import { useTranslation } from 'react-i18next';
-
-const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
- const { t } = useTranslation();
- // Wizard state
- const [step, setStep] = useState(1); // 1: Choose type, 2: Configure, 3: Confirm
- const [workspaceType, setWorkspaceType] = useState('existing'); // 'existing' or 'new' - default to 'existing'
-
- // Form state
- const [workspacePath, setWorkspacePath] = useState('');
- const [githubUrl, setGithubUrl] = useState('');
- const [selectedGithubToken, setSelectedGithubToken] = useState('');
- const [tokenMode, setTokenMode] = useState('stored'); // 'stored' | 'new' | 'none'
- const [newGithubToken, setNewGithubToken] = useState('');
-
- // UI state
- const [isCreating, setIsCreating] = useState(false);
- const [error, setError] = useState(null);
- const [availableTokens, setAvailableTokens] = useState([]);
- const [loadingTokens, setLoadingTokens] = useState(false);
- const [pathSuggestions, setPathSuggestions] = useState([]);
- const [showPathDropdown, setShowPathDropdown] = useState(false);
- const [showFolderBrowser, setShowFolderBrowser] = useState(false);
- const [browserCurrentPath, setBrowserCurrentPath] = useState('~');
- const [browserFolders, setBrowserFolders] = useState([]);
- const [loadingFolders, setLoadingFolders] = useState(false);
- const [showHiddenFolders, setShowHiddenFolders] = useState(false);
- const [showNewFolderInput, setShowNewFolderInput] = useState(false);
- const [newFolderName, setNewFolderName] = useState('');
- const [creatingFolder, setCreatingFolder] = useState(false);
- const [cloneProgress, setCloneProgress] = useState('');
-
- // Load available GitHub tokens when needed
- useEffect(() => {
- if (step === 2 && workspaceType === 'new' && githubUrl) {
- loadGithubTokens();
- }
- }, [step, workspaceType, githubUrl]);
-
- // Load path suggestions
- useEffect(() => {
- if (workspacePath.length > 2) {
- loadPathSuggestions(workspacePath);
- } else {
- setPathSuggestions([]);
- setShowPathDropdown(false);
- }
- }, [workspacePath]);
-
- const loadGithubTokens = async () => {
- try {
- setLoadingTokens(true);
- const response = await api.get('/settings/credentials?type=github_token');
- const data = await response.json();
-
- const activeTokens = (data.credentials || []).filter(t => t.is_active);
- setAvailableTokens(activeTokens);
-
- // Auto-select first token if available
- if (activeTokens.length > 0 && !selectedGithubToken) {
- setSelectedGithubToken(activeTokens[0].id.toString());
- }
- } catch (error) {
- console.error('Error loading GitHub tokens:', error);
- } finally {
- setLoadingTokens(false);
- }
- };
-
- const loadPathSuggestions = async (inputPath) => {
- try {
- // Extract the directory to browse (parent of input)
- const lastSlash = inputPath.lastIndexOf('/');
- const dirPath = lastSlash > 0 ? inputPath.substring(0, lastSlash) : '~';
-
- const response = await api.browseFilesystem(dirPath);
- const data = await response.json();
-
- if (data.suggestions) {
- // Filter suggestions based on the input, excluding exact match
- const filtered = data.suggestions.filter(s =>
- s.path.toLowerCase().startsWith(inputPath.toLowerCase()) &&
- s.path.toLowerCase() !== inputPath.toLowerCase()
- );
- setPathSuggestions(filtered.slice(0, 5));
- setShowPathDropdown(filtered.length > 0);
- }
- } catch (error) {
- console.error('Error loading path suggestions:', error);
- }
- };
-
- const handleNext = () => {
- setError(null);
-
- if (step === 1) {
- if (!workspaceType) {
- setError(t('projectWizard.errors.selectType'));
- return;
- }
- setStep(2);
- } else if (step === 2) {
- if (!workspacePath.trim()) {
- setError(t('projectWizard.errors.providePath'));
- return;
- }
-
- // No validation for GitHub token - it's optional (only needed for private repos)
- setStep(3);
- }
- };
-
- const handleBack = () => {
- setError(null);
- setStep(step - 1);
- };
-
- const handleCreate = async () => {
- setIsCreating(true);
- setError(null);
- setCloneProgress('');
-
- try {
- if (workspaceType === 'new' && githubUrl) {
- const params = new URLSearchParams({
- path: workspacePath.trim(),
- githubUrl: githubUrl.trim(),
- });
-
- if (tokenMode === 'stored' && selectedGithubToken) {
- params.append('githubTokenId', selectedGithubToken);
- } else if (tokenMode === 'new' && newGithubToken) {
- params.append('newGithubToken', newGithubToken.trim());
- }
-
- const token = localStorage.getItem('auth-token');
- const url = `/api/projects/clone-progress?${params}${token ? `&token=${token}` : ''}`;
-
- await new Promise((resolve, reject) => {
- const eventSource = new EventSource(url);
-
- eventSource.onmessage = (event) => {
- try {
- const data = JSON.parse(event.data);
-
- if (data.type === 'progress') {
- setCloneProgress(data.message);
- } else if (data.type === 'complete') {
- eventSource.close();
- if (onProjectCreated) {
- onProjectCreated(data.project);
- }
- onClose();
- resolve();
- } else if (data.type === 'error') {
- eventSource.close();
- reject(new Error(data.message));
- }
- } catch (e) {
- console.error('Error parsing SSE event:', e);
- }
- };
-
- eventSource.onerror = () => {
- eventSource.close();
- reject(new Error('Connection lost during clone'));
- };
- });
- return;
- }
-
- const payload = {
- workspaceType,
- path: workspacePath.trim(),
- };
-
- const response = await api.createWorkspace(payload);
- const data = await response.json();
-
- if (!response.ok) {
- throw new Error(data.details || data.error || t('projectWizard.errors.failedToCreate'));
- }
-
- if (onProjectCreated) {
- onProjectCreated(data.project);
- }
-
- onClose();
- } catch (error) {
- console.error('Error creating workspace:', error);
- setError(error.message || t('projectWizard.errors.failedToCreate'));
- } finally {
- setIsCreating(false);
- }
- };
-
- const selectPathSuggestion = (suggestion) => {
- setWorkspacePath(suggestion.path);
- setShowPathDropdown(false);
- };
-
- const openFolderBrowser = async () => {
- setShowFolderBrowser(true);
- await loadBrowserFolders('~');
- };
-
- const loadBrowserFolders = async (path) => {
- try {
- setLoadingFolders(true);
- const response = await api.browseFilesystem(path);
- const data = await response.json();
- setBrowserCurrentPath(data.path || path);
- setBrowserFolders(data.suggestions || []);
- } catch (error) {
- console.error('Error loading folders:', error);
- } finally {
- setLoadingFolders(false);
- }
- };
-
- const selectFolder = (folderPath, advanceToConfirm = false) => {
- setWorkspacePath(folderPath);
- setShowFolderBrowser(false);
- if (advanceToConfirm) {
- setStep(3);
- }
- };
-
- const navigateToFolder = async (folderPath) => {
- await loadBrowserFolders(folderPath);
- };
-
- const createNewFolder = async () => {
- if (!newFolderName.trim()) return;
- setCreatingFolder(true);
- setError(null);
- try {
- const separator = browserCurrentPath.includes('\\') ? '\\' : '/';
- const folderPath = `${browserCurrentPath}${separator}${newFolderName.trim()}`;
- const response = await api.createFolder(folderPath);
- const data = await response.json();
- if (!response.ok) {
- throw new Error(data.error || t('projectWizard.errors.failedToCreateFolder', 'Failed to create folder'));
- }
- setNewFolderName('');
- setShowNewFolderInput(false);
- await loadBrowserFolders(data.path || folderPath);
- } catch (error) {
- console.error('Error creating folder:', error);
- setError(error.message || t('projectWizard.errors.failedToCreateFolder', 'Failed to create folder'));
- } finally {
- setCreatingFolder(false);
- }
- };
-
- return (
-
-
- {/* Header */}
-
-
-
-
-
-
- {t('projectWizard.title')}
-
-
-
-
-
- {/* Progress Indicator */}
-
-
- {[1, 2, 3].map((s) => (
-
-
-
- {s < step ? : s}
-
-
- {s === 1 ? t('projectWizard.steps.type') : s === 2 ? t('projectWizard.steps.configure') : t('projectWizard.steps.confirm')}
-
-
- {s < 3 && (
-
- )}
-
- ))}
-
-
-
- {/* Content */}
-
- {/* Error Display */}
- {error && (
-
- )}
-
- {/* Step 1: Choose workspace type */}
- {step === 1 && (
-
-
-
- {t('projectWizard.step1.question')}
-
-
- {/* Existing Workspace */}
-
-
- {/* New Workspace */}
-
-
-
-
- )}
-
- {/* Step 2: Configure workspace */}
- {step === 2 && (
-
- {/* Workspace Path */}
-
-
-
-
-
setWorkspacePath(e.target.value)}
- placeholder={workspaceType === 'existing' ? '/path/to/existing/workspace' : '/path/to/new/workspace'}
- className="w-full"
- />
- {showPathDropdown && pathSuggestions.length > 0 && (
-
- {pathSuggestions.map((suggestion, index) => (
-
- ))}
-
- )}
-
-
-
-
- {workspaceType === 'existing'
- ? t('projectWizard.step2.existingHelp')
- : t('projectWizard.step2.newHelp')}
-
-
-
- {/* GitHub URL (only for new workspace) */}
- {workspaceType === 'new' && (
- <>
-
-
-
setGithubUrl(e.target.value)}
- placeholder="https://github.com/username/repository"
- className="w-full"
- />
-
- {t('projectWizard.step2.githubHelp')}
-
-
-
- {/* GitHub Token (only for HTTPS URLs - SSH uses SSH keys) */}
- {githubUrl && !githubUrl.startsWith('git@') && !githubUrl.startsWith('ssh://') && (
-
-
-
-
-
- {t('projectWizard.step2.githubAuth')}
-
-
- {t('projectWizard.step2.githubAuthHelp')}
-
-
-
-
- {loadingTokens ? (
-
-
- {t('projectWizard.step2.loadingTokens')}
-
- ) : availableTokens.length > 0 ? (
- <>
- {/* Token Selection Tabs */}
-
-
-
-
-
-
- {tokenMode === 'stored' ? (
-
-
-
-
- ) : tokenMode === 'new' ? (
-
-
-
setNewGithubToken(e.target.value)}
- placeholder="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
- className="w-full"
- />
-
- {t('projectWizard.step2.tokenHelp')}
-
-
- ) : null}
- >
- ) : (
-
-
-
- {t('projectWizard.step2.publicRepoInfo')}
-
-
-
-
-
-
setNewGithubToken(e.target.value)}
- placeholder={t('projectWizard.step2.tokenPublicPlaceholder')}
- className="w-full"
- />
-
- {t('projectWizard.step2.noTokensHelp')}
-
-
-
- )}
-
- )}
- >
- )}
-
- )}
-
- {/* Step 3: Confirm */}
- {step === 3 && (
-
-
-
- {t('projectWizard.step3.reviewConfig')}
-
-
-
- {t('projectWizard.step3.workspaceType')}
-
- {workspaceType === 'existing' ? t('projectWizard.step3.existingWorkspace') : t('projectWizard.step3.newWorkspace')}
-
-
-
- {t('projectWizard.step3.path')}
-
- {workspacePath}
-
-
- {workspaceType === 'new' && githubUrl && (
- <>
-
- {t('projectWizard.step3.cloneFrom')}
-
- {githubUrl}
-
-
-
- {t('projectWizard.step3.authentication')}
-
- {tokenMode === 'stored' && selectedGithubToken
- ? `${t('projectWizard.step3.usingStoredToken')} ${availableTokens.find(t => t.id.toString() === selectedGithubToken)?.credential_name || 'Unknown'}`
- : tokenMode === 'new' && newGithubToken
- ? t('projectWizard.step3.usingProvidedToken')
- : (githubUrl.startsWith('git@') || githubUrl.startsWith('ssh://'))
- ? t('projectWizard.step3.sshKey', 'SSH Key')
- : t('projectWizard.step3.noAuthentication')}
-
-
- >
- )}
-
-
-
-
- {isCreating && cloneProgress ? (
-
-
{t('projectWizard.step3.cloningRepository', 'Cloning repository...')}
-
- {cloneProgress}
-
-
- ) : (
-
- {workspaceType === 'existing'
- ? t('projectWizard.step3.existingInfo')
- : githubUrl
- ? t('projectWizard.step3.newWithClone')
- : t('projectWizard.step3.newEmpty')}
-
- )}
-
-
- )}
-
-
- {/* Footer */}
-
-
-
-
-
-
-
- {/* Folder Browser Modal */}
- {showFolderBrowser && (
-
-
- {/* Browser Header */}
-
-
-
-
-
-
- Select Folder
-
-
-
-
-
-
-
-
-
- {/* New Folder Input */}
- {showNewFolderInput && (
-
-
- setNewFolderName(e.target.value)}
- placeholder="New folder name"
- className="flex-1"
- onKeyDown={(e) => {
- if (e.key === 'Enter') createNewFolder();
- if (e.key === 'Escape') {
- setShowNewFolderInput(false);
- setNewFolderName('');
- }
- }}
- autoFocus
- />
-
-
-
-
- )}
-
- {/* Folder List */}
-
- {loadingFolders ? (
-
-
-
- ) : (
-
- {/* Parent Directory - check for Windows root (e.g., C:\) and Unix root */}
- {browserCurrentPath !== '~' && browserCurrentPath !== '/' && !/^[A-Za-z]:\\?$/.test(browserCurrentPath) && (
-
- )}
-
- {/* Folders */}
- {browserFolders.length === 0 ? (
-
- No subfolders found
-
- ) : (
- browserFolders
- .filter(folder => showHiddenFolders || !folder.name.startsWith('.'))
- .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
- .map((folder, index) => (
-
-
-
-
- ))
- )}
-
- )}
-
-
- {/* Browser Footer with Current Path */}
-
-
- Path:
-
- {browserCurrentPath}
-
-
-
-
-
-
-
-
-
- )}
-
- );
-};
-
-export default ProjectCreationWizard;
diff --git a/src/components/project-creation-wizard/ProjectCreationWizard.tsx b/src/components/project-creation-wizard/ProjectCreationWizard.tsx
new file mode 100644
index 00000000..ce490482
--- /dev/null
+++ b/src/components/project-creation-wizard/ProjectCreationWizard.tsx
@@ -0,0 +1,229 @@
+import { useCallback, useMemo, useState } from 'react';
+import { FolderPlus, X } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import ErrorBanner from './components/ErrorBanner';
+import StepConfiguration from './components/StepConfiguration';
+import StepReview from './components/StepReview';
+import StepTypeSelection from './components/StepTypeSelection';
+import WizardFooter from './components/WizardFooter';
+import WizardProgress from './components/WizardProgress';
+import { useGithubTokens } from './hooks/useGithubTokens';
+import { cloneWorkspaceWithProgress, createWorkspaceRequest } from './data/workspaceApi';
+import { isCloneWorkflow, shouldShowGithubAuthentication } from './utils/pathUtils';
+import type { TokenMode, WizardFormState, WizardStep, WorkspaceType } from './types';
+
+type ProjectCreationWizardProps = {
+ onClose: () => void;
+ onProjectCreated?: (project?: Record) => void;
+};
+
+const initialFormState: WizardFormState = {
+ workspaceType: 'existing',
+ workspacePath: '',
+ githubUrl: '',
+ tokenMode: 'stored',
+ selectedGithubToken: '',
+ newGithubToken: '',
+};
+
+export default function ProjectCreationWizard({
+ onClose,
+ onProjectCreated,
+}: ProjectCreationWizardProps) {
+ const { t } = useTranslation();
+ const [step, setStep] = useState(1);
+ const [formState, setFormState] = useState(initialFormState);
+ const [isCreating, setIsCreating] = useState(false);
+ const [error, setError] = useState(null);
+ const [cloneProgress, setCloneProgress] = useState('');
+
+ const shouldLoadTokens =
+ step === 2 && shouldShowGithubAuthentication(formState.workspaceType, formState.githubUrl);
+
+ const autoSelectToken = useCallback((tokenId: string) => {
+ setFormState((previous) => ({ ...previous, selectedGithubToken: tokenId }));
+ }, []);
+
+ const {
+ tokens: availableTokens,
+ loading: loadingTokens,
+ loadError: tokenLoadError,
+ selectedTokenName,
+ } = useGithubTokens({
+ shouldLoad: shouldLoadTokens,
+ selectedTokenId: formState.selectedGithubToken,
+ onAutoSelectToken: autoSelectToken,
+ });
+
+ // Keep cross-step values in this component; local UI state lives in child components.
+ const updateField = useCallback((key: K, value: WizardFormState[K]) => {
+ setFormState((previous) => ({ ...previous, [key]: value }));
+ }, []);
+
+ const updateWorkspaceType = useCallback(
+ (workspaceType: WorkspaceType) => updateField('workspaceType', workspaceType),
+ [updateField],
+ );
+
+ const updateTokenMode = useCallback(
+ (tokenMode: TokenMode) => updateField('tokenMode', tokenMode),
+ [updateField],
+ );
+
+ const handleNext = useCallback(() => {
+ setError(null);
+
+ if (step === 1) {
+ if (!formState.workspaceType) {
+ setError(t('projectWizard.errors.selectType'));
+ return;
+ }
+ setStep(2);
+ return;
+ }
+
+ if (step === 2) {
+ if (!formState.workspacePath.trim()) {
+ setError(t('projectWizard.errors.providePath'));
+ return;
+ }
+ setStep(3);
+ }
+ }, [formState.workspacePath, formState.workspaceType, step, t]);
+
+ const handleBack = useCallback(() => {
+ setError(null);
+ setStep((previousStep) => (previousStep > 1 ? ((previousStep - 1) as WizardStep) : previousStep));
+ }, []);
+
+ const handleCreate = useCallback(async () => {
+ setIsCreating(true);
+ setError(null);
+ setCloneProgress('');
+
+ try {
+ const shouldCloneRepository = isCloneWorkflow(formState.workspaceType, formState.githubUrl);
+
+ if (shouldCloneRepository) {
+ const project = await cloneWorkspaceWithProgress(
+ {
+ workspacePath: formState.workspacePath,
+ githubUrl: formState.githubUrl,
+ tokenMode: formState.tokenMode,
+ selectedGithubToken: formState.selectedGithubToken,
+ newGithubToken: formState.newGithubToken,
+ },
+ {
+ onProgress: setCloneProgress,
+ },
+ );
+
+ onProjectCreated?.(project);
+ onClose();
+ return;
+ }
+
+ const project = await createWorkspaceRequest({
+ workspaceType: formState.workspaceType,
+ path: formState.workspacePath.trim(),
+ });
+
+ onProjectCreated?.(project);
+ onClose();
+ } catch (createError) {
+ const errorMessage =
+ createError instanceof Error
+ ? createError.message
+ : t('projectWizard.errors.failedToCreate');
+ setError(errorMessage);
+ } finally {
+ setIsCreating(false);
+ }
+ }, [formState, onClose, onProjectCreated, t]);
+
+ const shouldCloneRepository = useMemo(
+ () => isCloneWorkflow(formState.workspaceType, formState.githubUrl),
+ [formState.githubUrl, formState.workspaceType],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+ {t('projectWizard.title')}
+
+
+
+
+
+
+
+
+ {error && }
+
+ {step === 1 && (
+
+ )}
+
+ {step === 2 && (
+ updateField('workspacePath', workspacePath)}
+ onGithubUrlChange={(githubUrl) => updateField('githubUrl', githubUrl)}
+ onTokenModeChange={updateTokenMode}
+ onSelectedGithubTokenChange={(selectedGithubToken) =>
+ updateField('selectedGithubToken', selectedGithubToken)
+ }
+ onNewGithubTokenChange={(newGithubToken) =>
+ updateField('newGithubToken', newGithubToken)
+ }
+ onAdvanceToConfirm={() => setStep(3)}
+ />
+ )}
+
+ {step === 3 && (
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/components/project-creation-wizard/components/ErrorBanner.tsx b/src/components/project-creation-wizard/components/ErrorBanner.tsx
new file mode 100644
index 00000000..3843cb21
--- /dev/null
+++ b/src/components/project-creation-wizard/components/ErrorBanner.tsx
@@ -0,0 +1,14 @@
+import { AlertCircle } from 'lucide-react';
+
+type ErrorBannerProps = {
+ message: string;
+};
+
+export default function ErrorBanner({ message }: ErrorBannerProps) {
+ return (
+
+ );
+}
diff --git a/src/components/project-creation-wizard/components/FolderBrowserModal.tsx b/src/components/project-creation-wizard/components/FolderBrowserModal.tsx
new file mode 100644
index 00000000..52448afa
--- /dev/null
+++ b/src/components/project-creation-wizard/components/FolderBrowserModal.tsx
@@ -0,0 +1,251 @@
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { Eye, EyeOff, FolderOpen, FolderPlus, Loader2, Plus, X } from 'lucide-react';
+import { Button, Input } from '../../../shared/view/ui';
+import { browseFilesystemFolders, createFolderInFilesystem } from '../data/workspaceApi';
+import { getParentPath, joinFolderPath } from '../utils/pathUtils';
+import type { FolderSuggestion } from '../types';
+
+type FolderBrowserModalProps = {
+ isOpen: boolean;
+ autoAdvanceOnSelect: boolean;
+ onClose: () => void;
+ onFolderSelected: (folderPath: string, advanceToConfirm: boolean) => void;
+};
+
+export default function FolderBrowserModal({
+ isOpen,
+ autoAdvanceOnSelect,
+ onClose,
+ onFolderSelected,
+}: FolderBrowserModalProps) {
+ const [currentPath, setCurrentPath] = useState('~');
+ const [folders, setFolders] = useState([]);
+ const [loadingFolders, setLoadingFolders] = useState(false);
+ const [showHiddenFolders, setShowHiddenFolders] = useState(false);
+ const [showNewFolderInput, setShowNewFolderInput] = useState(false);
+ const [newFolderName, setNewFolderName] = useState('');
+ const [creatingFolder, setCreatingFolder] = useState(false);
+ const [error, setError] = useState(null);
+
+ const loadFolders = useCallback(async (pathToLoad: string) => {
+ setLoadingFolders(true);
+ setError(null);
+
+ try {
+ const result = await browseFilesystemFolders(pathToLoad);
+ setCurrentPath(result.path);
+ setFolders(result.suggestions);
+ } catch (loadError) {
+ setError(loadError instanceof Error ? loadError.message : 'Failed to load folders');
+ } finally {
+ setLoadingFolders(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (!isOpen) {
+ return;
+ }
+ loadFolders('~');
+ }, [isOpen, loadFolders]);
+
+ const visibleFolders = useMemo(
+ () =>
+ folders
+ .filter((folder) => showHiddenFolders || !folder.name.startsWith('.'))
+ .sort((firstFolder, secondFolder) =>
+ firstFolder.name.toLowerCase().localeCompare(secondFolder.name.toLowerCase()),
+ ),
+ [folders, showHiddenFolders],
+ );
+
+ const resetNewFolderState = () => {
+ setShowNewFolderInput(false);
+ setNewFolderName('');
+ };
+
+ const handleClose = () => {
+ setError(null);
+ resetNewFolderState();
+ onClose();
+ };
+
+ const handleCreateFolder = useCallback(async () => {
+ if (!newFolderName.trim()) {
+ return;
+ }
+
+ setCreatingFolder(true);
+ setError(null);
+
+ try {
+ const folderPath = joinFolderPath(currentPath, newFolderName);
+ const createdPath = await createFolderInFilesystem(folderPath);
+ resetNewFolderState();
+ await loadFolders(createdPath);
+ } catch (createError) {
+ setError(createError instanceof Error ? createError.message : 'Failed to create folder');
+ } finally {
+ setCreatingFolder(false);
+ }
+ }, [currentPath, loadFolders, newFolderName]);
+
+ const parentPath = getParentPath(currentPath);
+
+ if (!isOpen) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {showNewFolderInput && (
+
+
+ setNewFolderName(event.target.value)}
+ placeholder="New folder name"
+ className="flex-1"
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ handleCreateFolder();
+ }
+ if (event.key === 'Escape') {
+ resetNewFolderState();
+ }
+ }}
+ autoFocus
+ />
+
+
+
+
+ )}
+
+ {error && (
+
+ )}
+
+
+ {loadingFolders ? (
+
+
+
+ ) : (
+
+ {parentPath && (
+
+ )}
+
+ {visibleFolders.length === 0 ? (
+
+ No subfolders found
+
+ ) : (
+ visibleFolders.map((folder) => (
+
+
+
+
+ ))
+ )}
+
+ )}
+
+
+
+
+ Path:
+
+ {currentPath}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/project-creation-wizard/components/GithubAuthenticationCard.tsx b/src/components/project-creation-wizard/components/GithubAuthenticationCard.tsx
new file mode 100644
index 00000000..7ce53a26
--- /dev/null
+++ b/src/components/project-creation-wizard/components/GithubAuthenticationCard.tsx
@@ -0,0 +1,161 @@
+import { Key, Loader2 } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { Input } from '../../../shared/view/ui';
+import type { GithubTokenCredential, TokenMode } from '../types';
+
+type GithubAuthenticationCardProps = {
+ tokenMode: TokenMode;
+ selectedGithubToken: string;
+ newGithubToken: string;
+ availableTokens: GithubTokenCredential[];
+ loadingTokens: boolean;
+ tokenLoadError: string | null;
+ onTokenModeChange: (tokenMode: TokenMode) => void;
+ onSelectedGithubTokenChange: (tokenId: string) => void;
+ onNewGithubTokenChange: (tokenValue: string) => void;
+};
+
+const getModeClassName = (mode: TokenMode, selectedMode: TokenMode) =>
+ `px-3 py-2 text-sm font-medium rounded-lg transition-colors ${
+ mode === selectedMode
+ ? mode === 'none'
+ ? 'bg-green-500 text-white'
+ : 'bg-blue-500 text-white'
+ : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
+ }`;
+
+export default function GithubAuthenticationCard({
+ tokenMode,
+ selectedGithubToken,
+ newGithubToken,
+ availableTokens,
+ loadingTokens,
+ tokenLoadError,
+ onTokenModeChange,
+ onSelectedGithubTokenChange,
+ onNewGithubTokenChange,
+}: GithubAuthenticationCardProps) {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+
+ {t('projectWizard.step2.githubAuth')}
+
+
+ {t('projectWizard.step2.githubAuthHelp')}
+
+
+
+
+ {loadingTokens && (
+
+
+ {t('projectWizard.step2.loadingTokens')}
+
+ )}
+
+ {!loadingTokens && tokenLoadError && (
+
{tokenLoadError}
+ )}
+
+ {!loadingTokens && availableTokens.length > 0 && (
+ <>
+
+
+
+
+
+
+ {tokenMode === 'stored' ? (
+
+
+
+
+ ) : tokenMode === 'new' ? (
+
+
+
onNewGithubTokenChange(event.target.value)}
+ placeholder="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ className="w-full"
+ />
+
+ {t('projectWizard.step2.tokenHelp')}
+
+
+ ) : null}
+ >
+ )}
+
+ {!loadingTokens && availableTokens.length === 0 && (
+
+
+
+ {t('projectWizard.step2.publicRepoInfo')}
+
+
+
+
+
+
{
+ const tokenValue = event.target.value;
+ onNewGithubTokenChange(tokenValue);
+ onTokenModeChange(tokenValue.trim() ? 'new' : 'none');
+ }}
+ placeholder={t('projectWizard.step2.tokenPublicPlaceholder')}
+ className="w-full"
+ />
+
+ {t('projectWizard.step2.noTokensHelp')}
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/project-creation-wizard/components/StepConfiguration.tsx b/src/components/project-creation-wizard/components/StepConfiguration.tsx
new file mode 100644
index 00000000..33234cdb
--- /dev/null
+++ b/src/components/project-creation-wizard/components/StepConfiguration.tsx
@@ -0,0 +1,108 @@
+import { useTranslation } from 'react-i18next';
+import { Input } from '../../../shared/view/ui';
+import GithubAuthenticationCard from './GithubAuthenticationCard';
+import WorkspacePathField from './WorkspacePathField';
+import { shouldShowGithubAuthentication } from '../utils/pathUtils';
+import type { GithubTokenCredential, TokenMode, WorkspaceType } from '../types';
+
+type StepConfigurationProps = {
+ workspaceType: WorkspaceType;
+ workspacePath: string;
+ githubUrl: string;
+ tokenMode: TokenMode;
+ selectedGithubToken: string;
+ newGithubToken: string;
+ availableTokens: GithubTokenCredential[];
+ loadingTokens: boolean;
+ tokenLoadError: string | null;
+ isCreating: boolean;
+ onWorkspacePathChange: (workspacePath: string) => void;
+ onGithubUrlChange: (githubUrl: string) => void;
+ onTokenModeChange: (tokenMode: TokenMode) => void;
+ onSelectedGithubTokenChange: (tokenId: string) => void;
+ onNewGithubTokenChange: (tokenValue: string) => void;
+ onAdvanceToConfirm: () => void;
+};
+
+export default function StepConfiguration({
+ workspaceType,
+ workspacePath,
+ githubUrl,
+ tokenMode,
+ selectedGithubToken,
+ newGithubToken,
+ availableTokens,
+ loadingTokens,
+ tokenLoadError,
+ isCreating,
+ onWorkspacePathChange,
+ onGithubUrlChange,
+ onTokenModeChange,
+ onSelectedGithubTokenChange,
+ onNewGithubTokenChange,
+ onAdvanceToConfirm,
+}: StepConfigurationProps) {
+ const { t } = useTranslation();
+ const showGithubAuth = shouldShowGithubAuthentication(workspaceType, githubUrl);
+
+ return (
+
+
+
+
+
+
+
+ {workspaceType === 'existing'
+ ? t('projectWizard.step2.existingHelp')
+ : t('projectWizard.step2.newHelp')}
+
+
+
+ {workspaceType === 'new' && (
+ <>
+
+
+
onGithubUrlChange(event.target.value)}
+ placeholder="https://github.com/username/repository"
+ className="w-full"
+ disabled={isCreating}
+ />
+
+ {t('projectWizard.step2.githubHelp')}
+
+
+
+ {showGithubAuth && (
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/src/components/project-creation-wizard/components/StepReview.tsx b/src/components/project-creation-wizard/components/StepReview.tsx
new file mode 100644
index 00000000..17201ac1
--- /dev/null
+++ b/src/components/project-creation-wizard/components/StepReview.tsx
@@ -0,0 +1,107 @@
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { isSshGitUrl } from '../utils/pathUtils';
+import type { WizardFormState } from '../types';
+
+type StepReviewProps = {
+ formState: WizardFormState;
+ selectedTokenName: string | null;
+ isCreating: boolean;
+ cloneProgress: string;
+};
+
+export default function StepReview({
+ formState,
+ selectedTokenName,
+ isCreating,
+ cloneProgress,
+}: StepReviewProps) {
+ const { t } = useTranslation();
+
+ const authenticationLabel = useMemo(() => {
+ if (formState.tokenMode === 'stored' && formState.selectedGithubToken) {
+ return `${t('projectWizard.step3.usingStoredToken')} ${selectedTokenName || 'Unknown'}`;
+ }
+
+ if (formState.tokenMode === 'new' && formState.newGithubToken.trim()) {
+ return t('projectWizard.step3.usingProvidedToken');
+ }
+
+ if (isSshGitUrl(formState.githubUrl)) {
+ return t('projectWizard.step3.sshKey', { defaultValue: 'SSH Key' });
+ }
+
+ return t('projectWizard.step3.noAuthentication');
+ }, [formState, selectedTokenName, t]);
+
+ return (
+
+
+
+ {t('projectWizard.step3.reviewConfig')}
+
+
+
+
+
+ {t('projectWizard.step3.workspaceType')}
+
+
+ {formState.workspaceType === 'existing'
+ ? t('projectWizard.step3.existingWorkspace')
+ : t('projectWizard.step3.newWorkspace')}
+
+
+
+
+ {t('projectWizard.step3.path')}
+
+ {formState.workspacePath}
+
+
+
+ {formState.workspaceType === 'new' && formState.githubUrl && (
+ <>
+
+
+ {t('projectWizard.step3.cloneFrom')}
+
+
+ {formState.githubUrl}
+
+
+
+
+
+ {t('projectWizard.step3.authentication')}
+
+ {authenticationLabel}
+
+ >
+ )}
+
+
+
+
+ {isCreating && cloneProgress ? (
+
+
+ {t('projectWizard.step3.cloningRepository', { defaultValue: 'Cloning repository...' })}
+
+
+ {cloneProgress}
+
+
+ ) : (
+
+ {formState.workspaceType === 'existing'
+ ? t('projectWizard.step3.existingInfo')
+ : formState.githubUrl
+ ? t('projectWizard.step3.newWithClone')
+ : t('projectWizard.step3.newEmpty')}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/project-creation-wizard/components/StepTypeSelection.tsx b/src/components/project-creation-wizard/components/StepTypeSelection.tsx
new file mode 100644
index 00000000..830aa270
--- /dev/null
+++ b/src/components/project-creation-wizard/components/StepTypeSelection.tsx
@@ -0,0 +1,71 @@
+import { FolderPlus, GitBranch } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import type { WorkspaceType } from '../types';
+
+type StepTypeSelectionProps = {
+ workspaceType: WorkspaceType;
+ onWorkspaceTypeChange: (workspaceType: WorkspaceType) => void;
+};
+
+export default function StepTypeSelection({
+ workspaceType,
+ onWorkspaceTypeChange,
+}: StepTypeSelectionProps) {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t('projectWizard.step1.question')}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/project-creation-wizard/components/WizardFooter.tsx b/src/components/project-creation-wizard/components/WizardFooter.tsx
new file mode 100644
index 00000000..689d1ea2
--- /dev/null
+++ b/src/components/project-creation-wizard/components/WizardFooter.tsx
@@ -0,0 +1,62 @@
+import { Check, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { Button } from '../../../shared/view/ui';
+import type { WizardStep } from '../types';
+
+type WizardFooterProps = {
+ step: WizardStep;
+ isCreating: boolean;
+ isCloneWorkflow: boolean;
+ onClose: () => void;
+ onBack: () => void;
+ onNext: () => void;
+ onCreate: () => void;
+};
+
+export default function WizardFooter({
+ step,
+ isCreating,
+ isCloneWorkflow,
+ onClose,
+ onBack,
+ onNext,
+ onCreate,
+}: WizardFooterProps) {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/project-creation-wizard/components/WizardProgress.tsx b/src/components/project-creation-wizard/components/WizardProgress.tsx
new file mode 100644
index 00000000..2615962c
--- /dev/null
+++ b/src/components/project-creation-wizard/components/WizardProgress.tsx
@@ -0,0 +1,52 @@
+import { Fragment } from 'react';
+import { Check } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import type { WizardStep } from '../types';
+
+type WizardProgressProps = {
+ step: WizardStep;
+};
+
+export default function WizardProgress({ step }: WizardProgressProps) {
+ const { t } = useTranslation();
+ const steps: WizardStep[] = [1, 2, 3];
+
+ return (
+
+
+ {steps.map((currentStep) => (
+
+
+
+ {currentStep < step ? : currentStep}
+
+
+ {currentStep === 1
+ ? t('projectWizard.steps.type')
+ : currentStep === 2
+ ? t('projectWizard.steps.configure')
+ : t('projectWizard.steps.confirm')}
+
+
+
+ {currentStep < 3 && (
+
+ )}
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/project-creation-wizard/components/WorkspacePathField.tsx b/src/components/project-creation-wizard/components/WorkspacePathField.tsx
new file mode 100644
index 00000000..711bad57
--- /dev/null
+++ b/src/components/project-creation-wizard/components/WorkspacePathField.tsx
@@ -0,0 +1,136 @@
+import { useCallback, useEffect, useState } from 'react';
+import { FolderOpen } from 'lucide-react';
+import { Button, Input } from '../../../shared/view/ui';
+import FolderBrowserModal from './FolderBrowserModal';
+import { browseFilesystemFolders } from '../data/workspaceApi';
+import { getSuggestionRootPath } from '../utils/pathUtils';
+import type { FolderSuggestion, WorkspaceType } from '../types';
+
+type WorkspacePathFieldProps = {
+ workspaceType: WorkspaceType;
+ value: string;
+ disabled?: boolean;
+ onChange: (path: string) => void;
+ onAdvanceToConfirm: () => void;
+};
+
+export default function WorkspacePathField({
+ workspaceType,
+ value,
+ disabled = false,
+ onChange,
+ onAdvanceToConfirm,
+}: WorkspacePathFieldProps) {
+ const [pathSuggestions, setPathSuggestions] = useState([]);
+ const [showPathDropdown, setShowPathDropdown] = useState(false);
+ const [showFolderBrowser, setShowFolderBrowser] = useState(false);
+
+ useEffect(() => {
+ if (value.trim().length <= 2) {
+ setPathSuggestions([]);
+ setShowPathDropdown(false);
+ return;
+ }
+
+ // Debounce path lookup to avoid firing a request for every keystroke.
+ const timerId = window.setTimeout(async () => {
+ try {
+ const directoryPath = getSuggestionRootPath(value);
+ const result = await browseFilesystemFolders(directoryPath);
+ const normalizedInput = value.toLowerCase();
+
+ const matchingSuggestions = result.suggestions
+ .filter((suggestion) => {
+ const normalizedSuggestion = suggestion.path.toLowerCase();
+ return (
+ normalizedSuggestion.startsWith(normalizedInput) &&
+ normalizedSuggestion !== normalizedInput
+ );
+ })
+ .slice(0, 5);
+
+ setPathSuggestions(matchingSuggestions);
+ setShowPathDropdown(matchingSuggestions.length > 0);
+ } catch (error) {
+ console.error('Failed to load path suggestions:', error);
+ }
+ }, 200);
+
+ return () => {
+ window.clearTimeout(timerId);
+ };
+ }, [value]);
+
+ const handleSuggestionSelect = useCallback(
+ (suggestion: FolderSuggestion) => {
+ onChange(suggestion.path);
+ setShowPathDropdown(false);
+ },
+ [onChange],
+ );
+
+ const handleFolderSelected = useCallback(
+ (selectedPath: string, advanceToConfirm: boolean) => {
+ onChange(selectedPath);
+ setShowFolderBrowser(false);
+ if (advanceToConfirm) {
+ onAdvanceToConfirm();
+ }
+ },
+ [onAdvanceToConfirm, onChange],
+ );
+
+ return (
+ <>
+
+
+
onChange(event.target.value)}
+ placeholder={
+ workspaceType === 'existing'
+ ? '/path/to/existing/workspace'
+ : '/path/to/new/workspace'
+ }
+ className="w-full"
+ disabled={disabled}
+ />
+
+ {showPathDropdown && pathSuggestions.length > 0 && (
+
+ {pathSuggestions.map((suggestion) => (
+
+ ))}
+
+ )}
+
+
+
+
+
+ setShowFolderBrowser(false)}
+ onFolderSelected={handleFolderSelected}
+ />
+ >
+ );
+}
diff --git a/src/components/project-creation-wizard/data/workspaceApi.ts b/src/components/project-creation-wizard/data/workspaceApi.ts
new file mode 100644
index 00000000..f4ca3baf
--- /dev/null
+++ b/src/components/project-creation-wizard/data/workspaceApi.ts
@@ -0,0 +1,150 @@
+import { api } from '../../../utils/api';
+import type {
+ BrowseFilesystemResponse,
+ CloneProgressEvent,
+ CreateFolderResponse,
+ CreateWorkspacePayload,
+ CreateWorkspaceResponse,
+ CredentialsResponse,
+ FolderSuggestion,
+ TokenMode,
+} from '../types';
+
+type CloneWorkspaceParams = {
+ workspacePath: string;
+ githubUrl: string;
+ tokenMode: TokenMode;
+ selectedGithubToken: string;
+ newGithubToken: string;
+};
+
+type CloneProgressHandlers = {
+ onProgress: (message: string) => void;
+};
+
+const parseJson = async (response: Response): Promise => {
+ const data = (await response.json()) as T;
+ return data;
+};
+
+export const fetchGithubTokenCredentials = async () => {
+ const response = await api.get('/settings/credentials?type=github_token');
+ const data = await parseJson(response);
+
+ if (!response.ok) {
+ throw new Error(data.error || 'Failed to load GitHub tokens');
+ }
+
+ return (data.credentials || []).filter((credential) => credential.is_active);
+};
+
+export const browseFilesystemFolders = async (pathToBrowse: string) => {
+ const endpoint = `/browse-filesystem?path=${encodeURIComponent(pathToBrowse)}`;
+ const response = await api.get(endpoint);
+ const data = await parseJson(response);
+
+ if (!response.ok) {
+ throw new Error(data.error || 'Failed to browse filesystem');
+ }
+
+ return {
+ path: data.path || pathToBrowse,
+ suggestions: (data.suggestions || []) as FolderSuggestion[],
+ };
+};
+
+export const createFolderInFilesystem = async (folderPath: string) => {
+ const response = await api.createFolder(folderPath);
+ const data = await parseJson(response);
+
+ if (!response.ok) {
+ throw new Error(data.error || 'Failed to create folder');
+ }
+
+ return data.path || folderPath;
+};
+
+export const createWorkspaceRequest = async (payload: CreateWorkspacePayload) => {
+ const response = await api.createWorkspace(payload);
+ const data = await parseJson(response);
+
+ if (!response.ok) {
+ throw new Error(data.details || data.error || 'Failed to create workspace');
+ }
+
+ return data.project;
+};
+
+const buildCloneProgressQuery = ({
+ workspacePath,
+ githubUrl,
+ tokenMode,
+ selectedGithubToken,
+ newGithubToken,
+}: CloneWorkspaceParams) => {
+ const query = new URLSearchParams({
+ path: workspacePath.trim(),
+ githubUrl: githubUrl.trim(),
+ });
+
+ if (tokenMode === 'stored' && selectedGithubToken) {
+ query.set('githubTokenId', selectedGithubToken);
+ }
+
+ if (tokenMode === 'new' && newGithubToken.trim()) {
+ query.set('newGithubToken', newGithubToken.trim());
+ }
+
+ // EventSource cannot send custom headers, so the auth token is passed as query.
+ const authToken = localStorage.getItem('auth-token');
+ if (authToken) {
+ query.set('token', authToken);
+ }
+
+ return query.toString();
+};
+
+export const cloneWorkspaceWithProgress = (
+ params: CloneWorkspaceParams,
+ handlers: CloneProgressHandlers,
+) =>
+ new Promise | undefined>((resolve, reject) => {
+ const query = buildCloneProgressQuery(params);
+ const eventSource = new EventSource(`/api/projects/clone-progress?${query}`);
+ let settled = false;
+
+ const settle = (callback: () => void) => {
+ if (settled) {
+ return;
+ }
+ settled = true;
+ eventSource.close();
+ callback();
+ };
+
+ eventSource.onmessage = (event) => {
+ try {
+ const payload = JSON.parse(event.data) as CloneProgressEvent;
+
+ if (payload.type === 'progress' && payload.message) {
+ handlers.onProgress(payload.message);
+ return;
+ }
+
+ if (payload.type === 'complete') {
+ settle(() => resolve(payload.project));
+ return;
+ }
+
+ if (payload.type === 'error') {
+ settle(() => reject(new Error(payload.message || 'Failed to clone repository')));
+ }
+ } catch (error) {
+ console.error('Error parsing clone progress event:', error);
+ }
+ };
+
+ eventSource.onerror = () => {
+ settle(() => reject(new Error('Connection lost during clone')));
+ };
+ });
diff --git a/src/components/project-creation-wizard/hooks/useGithubTokens.ts b/src/components/project-creation-wizard/hooks/useGithubTokens.ts
new file mode 100644
index 00000000..c1e2b4cf
--- /dev/null
+++ b/src/components/project-creation-wizard/hooks/useGithubTokens.ts
@@ -0,0 +1,73 @@
+import { useEffect, useMemo, useRef, useState } from 'react';
+import { fetchGithubTokenCredentials } from '../data/workspaceApi';
+import type { GithubTokenCredential } from '../types';
+
+type UseGithubTokensParams = {
+ shouldLoad: boolean;
+ selectedTokenId: string;
+ onAutoSelectToken: (tokenId: string) => void;
+};
+
+export const useGithubTokens = ({
+ shouldLoad,
+ selectedTokenId,
+ onAutoSelectToken,
+}: UseGithubTokensParams) => {
+ const [tokens, setTokens] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [loadError, setLoadError] = useState(null);
+ const hasLoadedRef = useRef(false);
+
+ useEffect(() => {
+ if (!shouldLoad || hasLoadedRef.current) {
+ return;
+ }
+
+ let isDisposed = false;
+
+ const loadTokens = async () => {
+ setLoading(true);
+ setLoadError(null);
+
+ try {
+ const activeTokens = await fetchGithubTokenCredentials();
+ if (isDisposed) {
+ return;
+ }
+
+ setTokens(activeTokens);
+ hasLoadedRef.current = true;
+
+ if (activeTokens.length > 0 && !selectedTokenId) {
+ onAutoSelectToken(String(activeTokens[0].id));
+ }
+ } catch (error) {
+ if (!isDisposed) {
+ setLoadError(error instanceof Error ? error.message : 'Failed to load GitHub tokens');
+ }
+ } finally {
+ if (!isDisposed) {
+ setLoading(false);
+ }
+ }
+ };
+
+ loadTokens();
+
+ return () => {
+ isDisposed = true;
+ };
+ }, [onAutoSelectToken, selectedTokenId, shouldLoad]);
+
+ const selectedTokenName = useMemo(
+ () => tokens.find((token) => String(token.id) === selectedTokenId)?.credential_name || null,
+ [selectedTokenId, tokens],
+ );
+
+ return {
+ tokens,
+ loading,
+ loadError,
+ selectedTokenName,
+ };
+};
diff --git a/src/components/project-creation-wizard/index.ts b/src/components/project-creation-wizard/index.ts
new file mode 100644
index 00000000..58d5b82e
--- /dev/null
+++ b/src/components/project-creation-wizard/index.ts
@@ -0,0 +1 @@
+export { default } from './ProjectCreationWizard';
diff --git a/src/components/project-creation-wizard/types.ts b/src/components/project-creation-wizard/types.ts
new file mode 100644
index 00000000..4b156303
--- /dev/null
+++ b/src/components/project-creation-wizard/types.ts
@@ -0,0 +1,62 @@
+export type WizardStep = 1 | 2 | 3;
+
+export type WorkspaceType = 'existing' | 'new';
+
+export type TokenMode = 'stored' | 'new' | 'none';
+
+export type FolderSuggestion = {
+ name: string;
+ path: string;
+ type?: string;
+};
+
+export type GithubTokenCredential = {
+ id: number;
+ credential_name: string;
+ is_active: boolean;
+};
+
+export type CredentialsResponse = {
+ credentials?: GithubTokenCredential[];
+ error?: string;
+};
+
+export type BrowseFilesystemResponse = {
+ path?: string;
+ suggestions?: FolderSuggestion[];
+ error?: string;
+};
+
+export type CreateFolderResponse = {
+ success?: boolean;
+ path?: string;
+ error?: string;
+ details?: string;
+};
+
+export type CreateWorkspacePayload = {
+ workspaceType: WorkspaceType;
+ path: string;
+};
+
+export type CreateWorkspaceResponse = {
+ success?: boolean;
+ project?: Record;
+ error?: string;
+ details?: string;
+};
+
+export type CloneProgressEvent = {
+ type?: string;
+ message?: string;
+ project?: Record;
+};
+
+export type WizardFormState = {
+ workspaceType: WorkspaceType;
+ workspacePath: string;
+ githubUrl: string;
+ tokenMode: TokenMode;
+ selectedGithubToken: string;
+ newGithubToken: string;
+};
diff --git a/src/components/project-creation-wizard/utils/pathUtils.ts b/src/components/project-creation-wizard/utils/pathUtils.ts
new file mode 100644
index 00000000..6799c3c1
--- /dev/null
+++ b/src/components/project-creation-wizard/utils/pathUtils.ts
@@ -0,0 +1,52 @@
+import type { WorkspaceType } from '../types';
+
+const SSH_PREFIXES = ['git@', 'ssh://'];
+const WINDOWS_DRIVE_PATTERN = /^[A-Za-z]:\\?$/;
+
+export const isSshGitUrl = (url: string): boolean => {
+ const trimmedUrl = url.trim();
+ return SSH_PREFIXES.some((prefix) => trimmedUrl.startsWith(prefix));
+};
+
+export const shouldShowGithubAuthentication = (
+ workspaceType: WorkspaceType,
+ githubUrl: string,
+): boolean => workspaceType === 'new' && githubUrl.trim().length > 0 && !isSshGitUrl(githubUrl);
+
+export const isCloneWorkflow = (workspaceType: WorkspaceType, githubUrl: string): boolean =>
+ workspaceType === 'new' && githubUrl.trim().length > 0;
+
+export const getSuggestionRootPath = (inputPath: string): string => {
+ const trimmedPath = inputPath.trim();
+ const lastSeparatorIndex = Math.max(trimmedPath.lastIndexOf('/'), trimmedPath.lastIndexOf('\\'));
+ if (lastSeparatorIndex === 2 && /^[A-Za-z]:/.test(trimmedPath)) {
+ return `${trimmedPath.slice(0, 2)}\\`;
+ }
+
+ return lastSeparatorIndex > 0 ? trimmedPath.slice(0, lastSeparatorIndex) : '~';
+};
+
+// Handles root edge cases for Unix-like and Windows paths.
+export const getParentPath = (currentPath: string): string | null => {
+ if (currentPath === '~' || currentPath === '/' || WINDOWS_DRIVE_PATTERN.test(currentPath)) {
+ return null;
+ }
+
+ const lastSeparatorIndex = Math.max(currentPath.lastIndexOf('/'), currentPath.lastIndexOf('\\'));
+ if (lastSeparatorIndex <= 0) {
+ return '/';
+ }
+
+ if (lastSeparatorIndex === 2 && /^[A-Za-z]:/.test(currentPath)) {
+ return `${currentPath.slice(0, 2)}\\`;
+ }
+
+ return currentPath.slice(0, lastSeparatorIndex);
+};
+
+export const joinFolderPath = (basePath: string, folderName: string): string => {
+ const normalizedBasePath = basePath.trim().replace(/[\\/]+$/, '');
+ const separator =
+ normalizedBasePath.includes('\\') && !normalizedBasePath.includes('/') ? '\\' : '/';
+ return `${normalizedBasePath}${separator}${folderName.trim()}`;
+};
diff --git a/src/components/sidebar/view/subcomponents/SidebarModals.tsx b/src/components/sidebar/view/subcomponents/SidebarModals.tsx
index e5bec6be..b12f9266 100644
--- a/src/components/sidebar/view/subcomponents/SidebarModals.tsx
+++ b/src/components/sidebar/view/subcomponents/SidebarModals.tsx
@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
import { AlertTriangle, Trash2 } from 'lucide-react';
import type { TFunction } from 'i18next';
import { Button } from '../../../../shared/view/ui';
-import ProjectCreationWizard from '../../../ProjectCreationWizard';
+
import Settings from '../../../settings/view/Settings';
import VersionUpgradeModal from '../../../version-upgrade/view';
import type { Project } from '../../../../types/app';
@@ -11,6 +11,7 @@ import type { ReleaseInfo } from '../../../../types/sharedTypes';
import type { InstallMode } from '../../../../hooks/useVersionCheck';
import { normalizeProjectForSettings } from '../../utils/utils';
import type { DeleteProjectConfirmation, SessionDeleteConfirmation, SettingsProject } from '../../types/types';
+import ProjectCreationWizard from '../../../project-creation-wizard';
type SidebarModalsProps = {
projects: Project[];