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) =>