diff --git a/package-lock.json b/package-lock.json index 769aeb5..1b089d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-code-ui", - "version": "1.7.0", + "version": "1.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-code-ui", - "version": "1.7.0", + "version": "1.8.0", "license": "MIT", "dependencies": { "@codemirror/lang-css": "^6.3.1", diff --git a/server/index.js b/server/index.js index f074c57..d69f825 100755 --- a/server/index.js +++ b/server/index.js @@ -301,6 +301,66 @@ app.post('/api/projects/create', authenticateToken, async (req, res) => { } }); +// Browse filesystem endpoint for project suggestions - uses existing getFileTree +app.get('/api/browse-filesystem', authenticateToken, async (req, res) => { + try { + const { path: dirPath } = req.query; + + // Default to home directory if no path provided + const homeDir = os.homedir(); + let targetPath = dirPath ? dirPath.replace('~', homeDir) : homeDir; + + // Resolve and normalize the path + targetPath = path.resolve(targetPath); + + // Security check - ensure path is accessible + try { + await fs.promises.access(targetPath); + const stats = await fs.promises.stat(targetPath); + + if (!stats.isDirectory()) { + return res.status(400).json({ error: 'Path is not a directory' }); + } + } catch (err) { + return res.status(404).json({ error: 'Directory not accessible' }); + } + + // Use existing getFileTree function with shallow depth (only direct children) + const fileTree = await getFileTree(targetPath, 1, 0, false); // maxDepth=1, showHidden=false + + // Filter only directories and format for suggestions + const directories = fileTree + .filter(item => item.type === 'directory') + .map(item => ({ + path: item.path, + name: item.name, + type: 'directory' + })) + .slice(0, 20); // Limit results + + // Add common directories if browsing home directory + const suggestions = []; + if (targetPath === homeDir) { + const commonDirs = ['Desktop', 'Documents', 'Projects', 'Development', 'Dev', 'Code', 'workspace']; + const existingCommon = directories.filter(dir => commonDirs.includes(dir.name)); + const otherDirs = directories.filter(dir => !commonDirs.includes(dir.name)); + + suggestions.push(...existingCommon, ...otherDirs); + } else { + suggestions.push(...directories); + } + + res.json({ + path: targetPath, + suggestions: suggestions + }); + + } catch (error) { + console.error('Error browsing filesystem:', error); + res.status(500).json({ error: 'Failed to browse filesystem' }); + } +}); + // Read file content endpoint app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) => { try { diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 4d1d4e2..71a6288 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { ScrollArea } from './ui/scroll-area'; import { Button } from './ui/button'; import { Badge } from './ui/badge'; @@ -72,6 +72,10 @@ 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(); @@ -176,6 +180,124 @@ 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(expandedProjects); if (newExpanded.has(projectName)) { @@ -347,8 +469,13 @@ function Sidebar({ 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) { @@ -371,6 +498,7 @@ function Sidebar({ const cancelNewProject = () => { setShowNewProject(false); setNewProjectPath(''); + setShowSuggestions(false); }; const loadMoreSessions = async (project) => { @@ -526,17 +654,96 @@ function Sidebar({ 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) => { - if (e.key === 'Enter') createNewProject(); - if (e.key === 'Escape') cancelNewProject(); - }} - /> +
+ 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} +
+
+
+
+ ))} +
+ )} +