From 0181883c8a5852ffa90d1743f780a5365fb393e1 Mon Sep 17 00:00:00 2001 From: simos Date: Tue, 4 Nov 2025 08:26:31 +0000 Subject: [PATCH] feat(projects): add project creation wizard with enhanced UX Add new project creation wizard component with improved user experience and better input field interactions. Integrate projects API routes on the backend to support CRUD operations for project management. Changes include: - Add ProjectCreationWizard component with step-by-step project setup - Improve input hint visibility to hide when user starts typing - Refactor project creation state management in Sidebar component - Add ReactDOM import for portal-based wizard rendering The wizard provides a more intuitive onboarding experience for users creating new projects, while the enhanced input hints reduce visual clutter during active typing. --- server/index.js | 4 + server/routes/projects.js | 215 +++++++++ src/components/ChatInterface.jsx | 6 +- src/components/ProjectCreationWizard.jsx | 570 +++++++++++++++++++++++ src/components/Sidebar.jsx | 451 +----------------- src/contexts/AuthContext.jsx | 7 +- src/index.css | 2 +- src/utils/api.js | 5 + 8 files changed, 828 insertions(+), 432 deletions(-) create mode 100644 server/routes/projects.js create mode 100644 src/components/ProjectCreationWizard.jsx diff --git a/server/index.js b/server/index.js index 4562f80..28ba951 100755 --- a/server/index.js +++ b/server/index.js @@ -69,6 +69,7 @@ import mcpUtilsRoutes from './routes/mcp-utils.js'; import commandsRoutes from './routes/commands.js'; import settingsRoutes from './routes/settings.js'; import agentRoutes from './routes/agent.js'; +import projectsRoutes from './routes/projects.js'; import { initializeDatabase } from './database/db.js'; import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js'; @@ -201,6 +202,9 @@ app.use('/api', validateApiKey); // Authentication routes (public) app.use('/api/auth', authRoutes); +// Projects API Routes (protected) +app.use('/api/projects', authenticateToken, projectsRoutes); + // Git API Routes (protected) app.use('/api/git', authenticateToken, gitRoutes); diff --git a/server/routes/projects.js b/server/routes/projects.js new file mode 100644 index 0000000..665f094 --- /dev/null +++ b/server/routes/projects.js @@ -0,0 +1,215 @@ +import express from 'express'; +import { promises as fs } from 'fs'; +import path from 'path'; +import { spawn } from 'child_process'; +import os from 'os'; +import { addProjectManually } from '../projects.js'; + +const router = express.Router(); + +/** + * Create a new workspace + * POST /api/projects/create-workspace + * + * Body: + * - workspaceType: 'existing' | 'new' + * - path: string (workspace path) + * - githubUrl?: string (optional, for new workspaces) + * - githubTokenId?: number (optional, ID of stored token) + * - newGithubToken?: string (optional, one-time token) + */ +router.post('/create-workspace', async (req, res) => { + try { + const { workspaceType, path: workspacePath, githubUrl, githubTokenId, newGithubToken } = req.body; + + // Validate required fields + if (!workspaceType || !workspacePath) { + return res.status(400).json({ error: 'workspaceType and path are required' }); + } + + if (!['existing', 'new'].includes(workspaceType)) { + return res.status(400).json({ error: 'workspaceType must be "existing" or "new"' }); + } + + const absolutePath = path.resolve(workspacePath); + + // Handle existing workspace + if (workspaceType === 'existing') { + // Check if the path exists + try { + await fs.access(absolutePath); + const stats = await fs.stat(absolutePath); + + if (!stats.isDirectory()) { + return res.status(400).json({ error: 'Path exists but is not a directory' }); + } + } catch (error) { + if (error.code === 'ENOENT') { + return res.status(404).json({ error: 'Workspace path does not exist' }); + } + throw error; + } + + // Add the existing workspace to the project list + const project = await addProjectManually(absolutePath); + + return res.json({ + success: true, + project, + message: 'Existing workspace added successfully' + }); + } + + // Handle new workspace creation + if (workspaceType === 'new') { + // Check if path already exists + try { + await fs.access(absolutePath); + return res.status(400).json({ + error: 'Path already exists. Please choose a different path or use "existing workspace" option.' + }); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + // Path doesn't exist - good, we can create it + } + + // Create the directory + await fs.mkdir(absolutePath, { recursive: true }); + + // If GitHub URL is provided, clone the repository + if (githubUrl) { + let githubToken = null; + + // Get GitHub token if needed + if (githubTokenId) { + // Fetch token from database + const token = await getGithubTokenById(githubTokenId, req.user.id); + if (!token) { + // Clean up created directory + await fs.rm(absolutePath, { recursive: true, force: true }); + return res.status(404).json({ error: 'GitHub token not found' }); + } + githubToken = token.github_token; + } else if (newGithubToken) { + githubToken = newGithubToken; + } + + // Clone the repository + try { + await cloneGitHubRepository(githubUrl, absolutePath, githubToken); + } catch (error) { + // Clean up created directory on failure + await fs.rm(absolutePath, { recursive: true, force: true }); + throw new Error(`Failed to clone repository: ${error.message}`); + } + } + + // Add the new workspace to the project list + const project = await addProjectManually(absolutePath); + + return res.json({ + success: true, + project, + message: githubUrl + ? 'New workspace created and repository cloned successfully' + : 'New workspace created successfully' + }); + } + + } catch (error) { + console.error('Error creating workspace:', error); + res.status(500).json({ + error: error.message || 'Failed to create workspace', + details: process.env.NODE_ENV === 'development' ? error.stack : undefined + }); + } +}); + +/** + * Helper function to get GitHub token from database + */ +async function getGithubTokenById(tokenId, userId) { + const { getDatabase } = await import('../database/db.js'); + const db = await getDatabase(); + + const token = await db.get( + 'SELECT * FROM github_tokens WHERE id = ? AND user_id = ? AND is_active = 1', + [tokenId, userId] + ); + + return token; +} + +/** + * Helper function to clone a GitHub repository + */ +function cloneGitHubRepository(githubUrl, destinationPath, githubToken = null) { + return new Promise((resolve, reject) => { + // Parse GitHub URL and inject token if provided + let cloneUrl = githubUrl; + + if (githubToken) { + try { + const url = new URL(githubUrl); + // Format: https://TOKEN@github.com/user/repo.git + url.username = githubToken; + url.password = ''; + cloneUrl = url.toString(); + } catch (error) { + return reject(new Error('Invalid GitHub URL format')); + } + } + + const gitProcess = spawn('git', ['clone', cloneUrl, destinationPath], { + stdio: ['ignore', 'pipe', 'pipe'], + env: { + ...process.env, + GIT_TERMINAL_PROMPT: '0' // Disable git password prompts + } + }); + + let stdout = ''; + let stderr = ''; + + gitProcess.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + gitProcess.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + gitProcess.on('close', (code) => { + if (code === 0) { + resolve({ stdout, stderr }); + } else { + // Parse git error messages to provide helpful feedback + let errorMessage = 'Git clone failed'; + + if (stderr.includes('Authentication failed') || stderr.includes('could not read Username')) { + errorMessage = 'Authentication failed. Please check your GitHub token.'; + } else if (stderr.includes('Repository not found')) { + errorMessage = 'Repository not found. Please check the URL and ensure you have access.'; + } else if (stderr.includes('already exists')) { + errorMessage = 'Directory already exists'; + } else if (stderr) { + errorMessage = stderr; + } + + reject(new Error(errorMessage)); + } + }); + + gitProcess.on('error', (error) => { + if (error.code === 'ENOENT') { + reject(new Error('Git is not installed or not in PATH')); + } else { + reject(error); + } + }); + }); +} + +export default router; diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index e5afb0b..5ecd99a 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -4710,13 +4710,15 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess {/* Hint text inside input box at bottom */} -
+
{sendByCtrlEnter ? "Ctrl+Enter to send • Tab for modes • / for commands" diff --git a/src/components/ProjectCreationWizard.jsx b/src/components/ProjectCreationWizard.jsx new file mode 100644 index 0000000..c85d589 --- /dev/null +++ b/src/components/ProjectCreationWizard.jsx @@ -0,0 +1,570 @@ +import React, { useState, useEffect } from 'react'; +import { X, FolderPlus, GitBranch, Key, ChevronRight, ChevronLeft, Check, Loader2, AlertCircle } from 'lucide-react'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; +import { api } from '../utils/api'; + +const ProjectCreationWizard = ({ onClose, onProjectCreated }) => { + // Wizard state + const [step, setStep] = useState(1); // 1: Choose type, 2: Configure, 3: Confirm + const [workspaceType, setWorkspaceType] = useState(null); // 'existing' or 'new' + + // Form state + const [workspacePath, setWorkspacePath] = useState(''); + const [githubUrl, setGithubUrl] = useState(''); + const [selectedGithubToken, setSelectedGithubToken] = useState(''); + const [useExistingToken, setUseExistingToken] = useState(true); + 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); + + // 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/github-tokens'); + const data = await response.json(); + + const activeTokens = (data.tokens || []).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 + const filtered = data.suggestions.filter(s => + s.path.toLowerCase().startsWith(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('Please select whether you have an existing workspace or want to create a new one'); + return; + } + setStep(2); + } else if (step === 2) { + if (!workspacePath.trim()) { + setError('Please provide a workspace path'); + 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); + + try { + const payload = { + workspaceType, + path: workspacePath.trim(), + }; + + // Add GitHub info if creating new workspace with GitHub URL + if (workspaceType === 'new' && githubUrl) { + payload.githubUrl = githubUrl.trim(); + + if (useExistingToken && selectedGithubToken) { + payload.githubTokenId = parseInt(selectedGithubToken); + } else if (!useExistingToken && newGithubToken) { + payload.newGithubToken = newGithubToken.trim(); + } + } + + const response = await api.createWorkspace(payload); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Failed to create workspace'); + } + + // Success! + if (onProjectCreated) { + onProjectCreated(data.project); + } + + onClose(); + } catch (error) { + console.error('Error creating workspace:', error); + setError(error.message || 'Failed to create workspace'); + } finally { + setIsCreating(false); + } + }; + + const selectPathSuggestion = (suggestion) => { + setWorkspacePath(suggestion.path); + setShowPathDropdown(false); + }; + + return ( +
+
+ {/* Header */} +
+
+
+ +
+

+ Create New Project +

+
+ +
+ + {/* Progress Indicator */} +
+
+ {[1, 2, 3].map((s) => ( + +
+
+ {s < step ? : s} +
+ + {s === 1 ? 'Type' : s === 2 ? 'Configure' : 'Confirm'} + +
+ {s < 3 && ( +
+ )} + + ))} +
+
+ + {/* Content */} +
+ {/* Error Display */} + {error && ( +
+ +
+

{error}

+
+
+ )} + + {/* Step 1: Choose workspace type */} + {step === 1 && ( +
+
+

+ Do you already have a workspace, or would you like to create a new one? +

+
+ {/* 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' + ? 'Full path to your existing workspace directory' + : 'Full path where the new workspace will be created'} +

+
+ + {/* GitHub URL (only for new workspace) */} + {workspaceType === 'new' && ( + <> +
+ + setGithubUrl(e.target.value)} + placeholder="https://github.com/username/repository" + className="w-full" + /> +

+ Leave empty to create an empty workspace, or provide a GitHub URL to clone +

+
+ + {/* GitHub Token (only if GitHub URL is provided) */} + {githubUrl && ( +
+
+ +
+
+ GitHub Authentication (Optional) +
+

+ Only required for private repositories. Public repos can be cloned without authentication. +

+
+
+ + {loadingTokens ? ( +
+ + Loading stored tokens... +
+ ) : availableTokens.length > 0 ? ( + <> + {/* Token Selection Tabs */} +
+ + + +
+ + {useExistingToken ? ( +
+ + +
+ ) : ( +
+ + setNewGithubToken(e.target.value)} + placeholder="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + className="w-full" + /> +

+ This token will be used only for this operation +

+
+ )} + + ) : ( +
+
+

+ 💡 Public repositories don't require authentication. You can skip providing a token if cloning a public repo. +

+
+ +
+ + setNewGithubToken(e.target.value)} + placeholder="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (leave empty for public repos)" + className="w-full" + /> +

+ No stored tokens available. You can add tokens in Settings → API Keys for easier reuse. +

+
+
+ )} +
+ )} + + )} +
+ )} + + {/* Step 3: Confirm */} + {step === 3 && ( +
+
+

+ Review Your Configuration +

+
+
+ Workspace Type: + + {workspaceType === 'existing' ? 'Existing Workspace' : 'New Workspace'} + +
+
+ Path: + + {workspacePath} + +
+ {workspaceType === 'new' && githubUrl && ( + <> +
+ Clone From: + + {githubUrl} + +
+
+ Authentication: + + {useExistingToken && selectedGithubToken + ? `Using stored token: ${availableTokens.find(t => t.id.toString() === selectedGithubToken)?.token_name || 'Unknown'}` + : newGithubToken + ? 'Using provided token' + : 'No authentication'} + +
+ + )} +
+
+ +
+

+ {workspaceType === 'existing' + ? 'The workspace will be added to your project list and will be available for Claude/Cursor sessions.' + : githubUrl + ? 'A new workspace will be created and the repository will be cloned from GitHub.' + : 'An empty workspace directory will be created at the specified path.'} +

+
+
+ )} +
+ + {/* Footer */} +
+ + + +
+
+
+ ); +}; + +export default ProjectCreationWizard; diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index cda6c5a..24e5bf7 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; +import ReactDOM from 'react-dom'; import { ScrollArea } from './ui/scroll-area'; import { Button } from './ui/button'; import { Badge } from './ui/badge'; @@ -9,6 +10,7 @@ import { cn } from '../lib/utils'; import ClaudeLogo from './ClaudeLogo'; import CursorLogo from './CursorLogo.jsx'; import TaskIndicator from './TaskIndicator'; +import ProjectCreationWizard from './ProjectCreationWizard'; import { api } from '../utils/api'; import { useTaskMaster } from '../contexts/TaskMasterContext'; import { useTasksSettings } from '../contexts/TasksSettingsContext'; @@ -64,8 +66,6 @@ function Sidebar({ const [editingProject, setEditingProject] = useState(null); const [showNewProject, setShowNewProject] = useState(false); const [editingName, setEditingName] = useState(''); - const [newProjectPath, setNewProjectPath] = useState(''); - const [creatingProject, setCreatingProject] = useState(false); const [loadingSessions, setLoadingSessions] = useState({}); const [additionalSessions, setAdditionalSessions] = useState({}); const [initialSessionsLoaded, setInitialSessionsLoaded] = useState(new Set()); @@ -76,10 +76,6 @@ function Sidebar({ const [editingSessionName, setEditingSessionName] = useState(''); const [generatingSummary, setGeneratingSummary] = useState({}); const [searchFilter, setSearchFilter] = useState(''); - const [showPathDropdown, setShowPathDropdown] = useState(false); - const [pathList, setPathList] = useState([]); - const [filteredPaths, setFilteredPaths] = useState([]); - const [selectedPathIndex, setSelectedPathIndex] = useState(-1); // TaskMaster context const { setCurrentProject, mcpServerStatus } = useTaskMaster(); @@ -184,123 +180,6 @@ function Sidebar({ }; }, []); - // Load available paths for suggestions - useEffect(() => { - const loadPaths = async () => { - try { - // Get recent paths from localStorage - const recentPaths = JSON.parse(localStorage.getItem('recentProjectPaths') || '[]'); - - // Load common/home directory paths - const response = await api.browseFilesystem(); - const data = await response.json(); - - if (data.suggestions) { - const homePaths = data.suggestions.map(s => ({ name: s.name, path: s.path })); - const allPaths = [...recentPaths.map(path => ({ name: path.split('/').pop(), path })), ...homePaths]; - setPathList(allPaths); - } else { - setPathList(recentPaths.map(path => ({ name: path.split('/').pop(), path }))); - } - } catch (error) { - console.error('Error loading paths:', error); - const recentPaths = JSON.parse(localStorage.getItem('recentProjectPaths') || '[]'); - setPathList(recentPaths.map(path => ({ name: path.split('/').pop(), path }))); - } - }; - - loadPaths(); - }, []); - - // Handle input change and path filtering with dynamic browsing (ChatInterface pattern + dynamic browsing) - useEffect(() => { - const inputValue = newProjectPath.trim(); - - if (inputValue.length === 0) { - setShowPathDropdown(false); - return; - } - - // Show dropdown when user starts typing - setShowPathDropdown(true); - - const updateSuggestions = async () => { - // First show filtered existing suggestions from pathList - const staticFiltered = pathList.filter(pathItem => - pathItem.name.toLowerCase().includes(inputValue.toLowerCase()) || - pathItem.path.toLowerCase().includes(inputValue.toLowerCase()) - ); - - // Check if input looks like a directory path for dynamic browsing - const isDirPath = inputValue.includes('/') && inputValue.length > 1; - - if (isDirPath) { - try { - let dirToSearch; - - // Determine which directory to search - if (inputValue.endsWith('/')) { - // User typed "/home/simos/" - search inside /home/simos - dirToSearch = inputValue.slice(0, -1); - } else { - // User typed "/home/simos/con" - search inside /home/simos for items starting with "con" - const lastSlashIndex = inputValue.lastIndexOf('/'); - dirToSearch = inputValue.substring(0, lastSlashIndex); - } - - // Only search if we have a valid directory path (not root only) - if (dirToSearch && dirToSearch !== '') { - const response = await api.browseFilesystem(dirToSearch); - const data = await response.json(); - - if (data.suggestions) { - // Filter directories that match the current input - const partialName = inputValue.substring(inputValue.lastIndexOf('/') + 1); - const dynamicPaths = data.suggestions - .filter(suggestion => { - const dirName = suggestion.name; - return partialName ? dirName.toLowerCase().startsWith(partialName.toLowerCase()) : true; - }) - .map(s => ({ name: s.name, path: s.path })) - .slice(0, 8); - - // Combine static and dynamic suggestions, prioritize dynamic - const combined = [...dynamicPaths, ...staticFiltered].slice(0, 8); - setFilteredPaths(combined); - setSelectedPathIndex(-1); - return; - } - } - } catch (error) { - console.debug('Dynamic browsing failed:', error.message); - } - } - - // Fallback to just static filtered suggestions - setFilteredPaths(staticFiltered.slice(0, 8)); - setSelectedPathIndex(-1); - }; - - updateSuggestions(); - }, [newProjectPath, pathList]); - - // Select path from dropdown (ChatInterface pattern) - const selectPath = (pathItem) => { - setNewProjectPath(pathItem.path); - setShowPathDropdown(false); - setSelectedPathIndex(-1); - }; - - // Save path to recent paths - const saveToRecentPaths = (path) => { - try { - const recentPaths = JSON.parse(localStorage.getItem('recentProjectPaths') || '[]'); - const updatedPaths = [path, ...recentPaths.filter(p => p !== path)].slice(0, 10); - localStorage.setItem('recentProjectPaths', JSON.stringify(updatedPaths)); - } catch (error) { - console.error('Error saving recent paths:', error); - } - }; const toggleProject = (projectName) => { const newExpanded = new Set(); @@ -465,50 +344,6 @@ function Sidebar({ } }; - const createNewProject = async () => { - if (!newProjectPath.trim()) { - alert('Please enter a project path'); - return; - } - - setCreatingProject(true); - - try { - const response = await api.createProject(newProjectPath.trim()); - - if (response.ok) { - const result = await response.json(); - - // Save the path to recent paths before clearing - saveToRecentPaths(newProjectPath.trim()); - - setShowNewProject(false); - setNewProjectPath(''); - setShowSuggestions(false); - - // Refresh projects to show the new one - if (window.refreshProjects) { - window.refreshProjects(); - } else { - window.location.reload(); - } - } else { - const error = await response.json(); - alert(error.error || 'Failed to create project. Please try again.'); - } - } catch (error) { - console.error('Error creating project:', error); - alert('Error creating project. Please try again.'); - } finally { - setCreatingProject(false); - } - }; - - const cancelNewProject = () => { - setShowNewProject(false); - setNewProjectPath(''); - setShowSuggestions(false); - }; const loadMoreSessions = async (project) => { // Check if we can load more sessions @@ -571,10 +406,27 @@ function Sidebar({ }; return ( -
+ <> + {/* Project Creation Wizard Modal - Rendered via Portal at document root for full-screen on mobile */} + {showNewProject && ReactDOM.createPortal( + setShowNewProject(false)} + onProjectCreated={(project) => { + // Refresh projects list after creation + if (window.refreshProjects) { + window.refreshProjects(); + } else { + window.location.reload(); + } + }} + />, + document.body + )} + +
{/* Header */}
{/* Desktop Header */} @@ -648,263 +500,7 @@ function Sidebar({
- - {/* New Project Form */} - {showNewProject && ( -
- {/* Desktop Form */} -
-
- - Create New Project -
-
- setNewProjectPath(e.target.value)} - placeholder="/path/to/project or relative/path" - className="text-sm focus:ring-2 focus:ring-primary/20" - autoFocus - onKeyDown={(e) => { - // Handle path dropdown navigation (ChatInterface pattern) - if (showPathDropdown && filteredPaths.length > 0) { - if (e.key === 'ArrowDown') { - e.preventDefault(); - setSelectedPathIndex(prev => - prev < filteredPaths.length - 1 ? prev + 1 : 0 - ); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - setSelectedPathIndex(prev => - prev > 0 ? prev - 1 : filteredPaths.length - 1 - ); - } else if (e.key === 'Enter') { - e.preventDefault(); - if (selectedPathIndex >= 0) { - selectPath(filteredPaths[selectedPathIndex]); - } else if (filteredPaths.length > 0) { - selectPath(filteredPaths[0]); - } else { - createNewProject(); - } - return; - } else if (e.key === 'Escape') { - e.preventDefault(); - setShowPathDropdown(false); - return; - } else if (e.key === 'Tab') { - e.preventDefault(); - if (selectedPathIndex >= 0) { - selectPath(filteredPaths[selectedPathIndex]); - } else if (filteredPaths.length > 0) { - selectPath(filteredPaths[0]); - } - return; - } - } - - // Regular input handling - if (e.key === 'Enter') { - createNewProject(); - } - if (e.key === 'Escape') { - cancelNewProject(); - } - }} - /> - - {/* Path dropdown (ChatInterface pattern) */} - {showPathDropdown && filteredPaths.length > 0 && ( -
- {filteredPaths.map((pathItem, index) => ( -
{ - e.preventDefault(); - e.stopPropagation(); - }} - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - selectPath(pathItem); - }} - > -
- -
-
{pathItem.name}
-
- {pathItem.path} -
-
-
-
- ))} -
- )} -
-
- - -
-
- - {/* Mobile Form - Simple Overlay */} -
-
-
-
-
- -
-
-

New Project

-
-
- -
- -
-
- setNewProjectPath(e.target.value)} - placeholder="/path/to/project or relative/path" - className="text-sm h-10 rounded-md focus:border-primary transition-colors" - autoFocus - onKeyDown={(e) => { - // Handle path dropdown navigation (same as desktop) - if (showPathDropdown && filteredPaths.length > 0) { - if (e.key === 'ArrowDown') { - e.preventDefault(); - setSelectedPathIndex(prev => - prev < filteredPaths.length - 1 ? prev + 1 : 0 - ); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - setSelectedPathIndex(prev => - prev > 0 ? prev - 1 : filteredPaths.length - 1 - ); - } else if (e.key === 'Enter') { - e.preventDefault(); - if (selectedPathIndex >= 0) { - selectPath(filteredPaths[selectedPathIndex]); - } else if (filteredPaths.length > 0) { - selectPath(filteredPaths[0]); - } else { - createNewProject(); - } - return; - } else if (e.key === 'Escape') { - e.preventDefault(); - setShowPathDropdown(false); - return; - } - } - - // Regular input handling - if (e.key === 'Enter') { - createNewProject(); - } - if (e.key === 'Escape') { - cancelNewProject(); - } - }} - style={{ - fontSize: '16px', // Prevents zoom on iOS - WebkitAppearance: 'none' - }} - /> - - {/* Mobile Path dropdown */} - {showPathDropdown && filteredPaths.length > 0 && ( -
- {filteredPaths.map((pathItem, index) => ( -
{ - e.preventDefault(); - e.stopPropagation(); - }} - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - selectPath(pathItem); - }} - > -
- -
-
{pathItem.name}
-
- {pathItem.path} -
-
-
-
- ))} -
- )} -
- -
- - -
-
- - {/* Safe area for mobile */} -
-
-
-
- )} - {/* Search Filter and Actions */} {projects.length > 0 && !isLoading && (
@@ -1693,6 +1289,7 @@ function Sidebar({
+ ); } diff --git a/src/contexts/AuthContext.jsx b/src/contexts/AuthContext.jsx index 77acb6c..c227814 100644 --- a/src/contexts/AuthContext.jsx +++ b/src/contexts/AuthContext.jsx @@ -34,12 +34,14 @@ export const AuthProvider = ({ children }) => { const checkAuthStatus = async () => { try { + console.log('[AuthContext] Checking auth status...'); setIsLoading(true); setError(null); - + // Check if system needs setup const statusResponse = await api.auth.status(); const statusData = await statusResponse.json(); + console.log('[AuthContext] Status response:', statusData); if (statusData.needsSetup) { setNeedsSetup(true); @@ -70,9 +72,10 @@ export const AuthProvider = ({ children }) => { } } } catch (error) { - console.error('Auth status check failed:', error); + console.error('[AuthContext] Auth status check failed:', error); setError('Failed to check authentication status'); } finally { + console.log('[AuthContext] Auth check complete, isLoading=false'); setIsLoading(false); } }; diff --git a/src/index.css b/src/index.css index d47186a..90abc99 100644 --- a/src/index.css +++ b/src/index.css @@ -79,7 +79,7 @@ --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; + --input: 220 13% 46%; --ring: 217.2 91.2% 59.8%; } } diff --git a/src/utils/api.js b/src/utils/api.js index e3a93d3..9459417 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -71,6 +71,11 @@ export const api = { method: 'POST', body: JSON.stringify({ path }), }), + createWorkspace: (workspaceData) => + authenticatedFetch('/api/projects/create-workspace', { + method: 'POST', + body: JSON.stringify(workspaceData), + }), readFile: (projectName, filePath) => authenticatedFetch(`/api/projects/${projectName}/file?filePath=${encodeURIComponent(filePath)}`), saveFile: (projectName, filePath, content) =>