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({