diff --git a/server/index.js b/server/index.js
index 3baeed8..650e216 100755
--- a/server/index.js
+++ b/server/index.js
@@ -820,6 +820,14 @@ app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../dist/index.html'));
});
+// Helper function to convert permissions to rwx format
+function permToRwx(perm) {
+ const r = perm & 4 ? 'r' : '-';
+ const w = perm & 2 ? 'w' : '-';
+ const x = perm & 1 ? 'x' : '-';
+ return r + w + x;
+}
+
async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden = true) {
// Using fsPromises from import
const items = [];
@@ -836,12 +844,34 @@ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden =
entry.name === 'dist' ||
entry.name === 'build') continue;
+ const itemPath = path.join(dirPath, entry.name);
const item = {
name: entry.name,
- path: path.join(dirPath, entry.name),
+ path: itemPath,
type: entry.isDirectory() ? 'directory' : 'file'
};
+ // Get file stats for additional metadata
+ try {
+ const stats = await fsPromises.stat(itemPath);
+ item.size = stats.size;
+ item.modified = stats.mtime.toISOString();
+
+ // Convert permissions to rwx format
+ const mode = stats.mode;
+ const ownerPerm = (mode >> 6) & 7;
+ const groupPerm = (mode >> 3) & 7;
+ const otherPerm = mode & 7;
+ item.permissions = ((mode >> 6) & 7).toString() + ((mode >> 3) & 7).toString() + (mode & 7).toString();
+ item.permissionsRwx = permToRwx(ownerPerm) + permToRwx(groupPerm) + permToRwx(otherPerm);
+ } catch (statError) {
+ // If stat fails, provide default values
+ item.size = 0;
+ item.modified = null;
+ item.permissions = '000';
+ item.permissionsRwx = '---------';
+ }
+
if (entry.isDirectory() && currentDepth < maxDepth) {
// Recursively get subdirectories but limit depth
try {
diff --git a/src/components/FileTree.jsx b/src/components/FileTree.jsx
index c2328db..1048bdb 100755
--- a/src/components/FileTree.jsx
+++ b/src/components/FileTree.jsx
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { ScrollArea } from './ui/scroll-area';
import { Button } from './ui/button';
-import { Folder, FolderOpen, File, FileText, FileCode } from 'lucide-react';
+import { Folder, FolderOpen, File, FileText, FileCode, List, TableProperties, Eye } from 'lucide-react';
import { cn } from '../lib/utils';
import CodeEditor from './CodeEditor';
import ImageViewer from './ImageViewer';
@@ -13,6 +13,7 @@ function FileTree({ selectedProject }) {
const [expandedDirs, setExpandedDirs] = useState(new Set());
const [selectedFile, setSelectedFile] = useState(null);
const [selectedImage, setSelectedImage] = useState(null);
+ const [viewMode, setViewMode] = useState('detailed'); // 'simple', 'detailed', 'compact'
useEffect(() => {
if (selectedProject) {
@@ -20,6 +21,14 @@ function FileTree({ selectedProject }) {
}
}, [selectedProject]);
+ // Load view mode preference from localStorage
+ useEffect(() => {
+ const savedViewMode = localStorage.getItem('file-tree-view-mode');
+ if (savedViewMode && ['simple', 'detailed', 'compact'].includes(savedViewMode)) {
+ setViewMode(savedViewMode);
+ }
+ }, []);
+
const fetchFiles = async () => {
setLoading(true);
try {
@@ -52,6 +61,35 @@ function FileTree({ selectedProject }) {
setExpandedDirs(newExpanded);
};
+ // Change view mode and save preference
+ const changeViewMode = (mode) => {
+ setViewMode(mode);
+ localStorage.setItem('file-tree-view-mode', mode);
+ };
+
+ // Format file size
+ const formatFileSize = (bytes) => {
+ if (!bytes || bytes === 0) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
+ };
+
+ // Format date as relative time
+ const formatRelativeTime = (date) => {
+ if (!date) return '-';
+ const now = new Date();
+ const past = new Date(date);
+ const diffInSeconds = Math.floor((now - past) / 1000);
+
+ if (diffInSeconds < 60) return 'just now';
+ if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} min ago`;
+ if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
+ if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)} days ago`;
+ return past.toLocaleDateString();
+ };
+
const renderFileTree = (items, level = 0) => {
return items.map((item) => (
@@ -135,6 +173,129 @@ function FileTree({ selectedProject }) {
}
};
+ // Render detailed view with table-like layout
+ const renderDetailedView = (items, level = 0) => {
+ return items.map((item) => (
+
+
{
+ if (item.type === 'directory') {
+ toggleDirectory(item.path);
+ } else if (isImageFile(item.name)) {
+ setSelectedImage({
+ name: item.name,
+ path: item.path,
+ projectPath: selectedProject.path,
+ projectName: selectedProject.name
+ });
+ } else {
+ setSelectedFile({
+ name: item.name,
+ path: item.path,
+ projectPath: selectedProject.path,
+ projectName: selectedProject.name
+ });
+ }
+ }}
+ >
+
+ {item.type === 'directory' ? (
+ expandedDirs.has(item.path) ? (
+
+ ) : (
+
+ )
+ ) : (
+ getFileIcon(item.name)
+ )}
+
+ {item.name}
+
+
+
+ {item.type === 'file' ? formatFileSize(item.size) : '-'}
+
+
+ {formatRelativeTime(item.modified)}
+
+
+ {item.permissionsRwx || '-'}
+
+
+
+ {item.type === 'directory' &&
+ expandedDirs.has(item.path) &&
+ item.children &&
+ renderDetailedView(item.children, level + 1)}
+
+ ));
+ };
+
+ // Render compact view with inline details
+ const renderCompactView = (items, level = 0) => {
+ return items.map((item) => (
+
+
{
+ if (item.type === 'directory') {
+ toggleDirectory(item.path);
+ } else if (isImageFile(item.name)) {
+ setSelectedImage({
+ name: item.name,
+ path: item.path,
+ projectPath: selectedProject.path,
+ projectName: selectedProject.name
+ });
+ } else {
+ setSelectedFile({
+ name: item.name,
+ path: item.path,
+ projectPath: selectedProject.path,
+ projectName: selectedProject.name
+ });
+ }
+ }}
+ >
+
+ {item.type === 'directory' ? (
+ expandedDirs.has(item.path) ? (
+
+ ) : (
+
+ )
+ ) : (
+ getFileIcon(item.name)
+ )}
+
+ {item.name}
+
+
+
+ {item.type === 'file' && (
+ <>
+ {formatFileSize(item.size)}
+ {item.permissionsRwx}
+ >
+ )}
+
+
+
+ {item.type === 'directory' &&
+ expandedDirs.has(item.path) &&
+ item.children &&
+ renderCompactView(item.children, level + 1)}
+
+ ));
+ };
+
if (loading) {
return (
@@ -147,6 +308,51 @@ function FileTree({ selectedProject }) {
return (
+ {/* View Mode Toggle */}
+
+
Files
+
+
changeViewMode('simple')}
+ title="Simple view"
+ >
+
+
+
changeViewMode('compact')}
+ title="Compact view"
+ >
+
+
+
changeViewMode('detailed')}
+ title="Detailed view"
+ >
+
+
+
+
+
+ {/* Column Headers for Detailed View */}
+ {viewMode === 'detailed' && files.length > 0 && (
+
+
+
Name
+
Size
+
Modified
+
Permissions
+
+
+ )}
{files.length === 0 ? (
@@ -160,8 +366,10 @@ function FileTree({ selectedProject }) {
) : (
-
- {renderFileTree(files)}
+
+ {viewMode === 'simple' && renderFileTree(files)}
+ {viewMode === 'compact' && renderCompactView(files)}
+ {viewMode === 'detailed' && renderDetailedView(files)}
)}
diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx
index d076428..5e9c566 100755
--- a/src/components/Sidebar.jsx
+++ b/src/components/Sidebar.jsx
@@ -61,6 +61,7 @@ function Sidebar({
const [additionalSessions, setAdditionalSessions] = useState({});
const [initialSessionsLoaded, setInitialSessionsLoaded] = useState(new Set());
const [currentTime, setCurrentTime] = useState(new Date());
+ const [projectSortOrder, setProjectSortOrder] = useState('name');
const [isRefreshing, setIsRefreshing] = useState(false);
const [editingSession, setEditingSession] = useState(null);
const [editingSessionName, setEditingSessionName] = useState('');
@@ -125,6 +126,45 @@ function Sidebar({
}
}, [projects, isLoading]);
+ // Load project sort order from settings
+ useEffect(() => {
+ const loadSortOrder = () => {
+ try {
+ const savedSettings = localStorage.getItem('claude-tools-settings');
+ if (savedSettings) {
+ const settings = JSON.parse(savedSettings);
+ setProjectSortOrder(settings.projectSortOrder || 'name');
+ }
+ } catch (error) {
+ console.error('Error loading sort order:', error);
+ }
+ };
+
+ // Load initially
+ loadSortOrder();
+
+ // Listen for storage changes
+ const handleStorageChange = (e) => {
+ if (e.key === 'claude-tools-settings') {
+ loadSortOrder();
+ }
+ };
+
+ window.addEventListener('storage', handleStorageChange);
+
+ // Also check periodically when component is focused (for same-tab changes)
+ const checkInterval = setInterval(() => {
+ if (document.hasFocus()) {
+ loadSortOrder();
+ }
+ }, 1000);
+
+ return () => {
+ window.removeEventListener('storage', handleStorageChange);
+ clearInterval(checkInterval);
+ };
+ }, []);
+
const toggleProject = (projectName) => {
const newExpanded = new Set(expandedProjects);
if (newExpanded.has(projectName)) {
@@ -333,6 +373,35 @@ function Sidebar({
return [...initialSessions, ...additional];
};
+ // Helper function to get the last activity date for a project
+ const getProjectLastActivity = (project) => {
+ const allSessions = getAllSessions(project);
+ if (allSessions.length === 0) {
+ return new Date(0); // Return epoch date for projects with no sessions
+ }
+
+ // Find the most recent session activity
+ const mostRecentDate = allSessions.reduce((latest, session) => {
+ const sessionDate = new Date(session.lastActivity);
+ return sessionDate > latest ? sessionDate : latest;
+ }, new Date(0));
+
+ return mostRecentDate;
+ };
+
+ // Sort projects based on selected order
+ const sortedProjects = [...projects].sort((a, b) => {
+ if (projectSortOrder === 'date') {
+ // Sort by most recent activity (descending)
+ return getProjectLastActivity(b) - getProjectLastActivity(a);
+ } else {
+ // Sort by display name (user-defined) or fallback to name (ascending)
+ const nameA = a.displayName || a.name;
+ const nameB = b.displayName || b.name;
+ return nameA.localeCompare(nameB);
+ }
+ });
+
return (
{/* Header */}
diff --git a/src/components/ToolsSettings.jsx b/src/components/ToolsSettings.jsx
index da4aae2..ff98a3e 100755
--- a/src/components/ToolsSettings.jsx
+++ b/src/components/ToolsSettings.jsx
@@ -15,6 +15,7 @@ function ToolsSettings({ isOpen, onClose }) {
const [skipPermissions, setSkipPermissions] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [saveStatus, setSaveStatus] = useState(null);
+ const [projectSortOrder, setProjectSortOrder] = useState('name');
// Common tool patterns
const commonTools = [
@@ -51,11 +52,13 @@ function ToolsSettings({ isOpen, onClose }) {
setAllowedTools(settings.allowedTools || []);
setDisallowedTools(settings.disallowedTools || []);
setSkipPermissions(settings.skipPermissions || false);
+ setProjectSortOrder(settings.projectSortOrder || 'name');
} else {
// Set defaults
setAllowedTools([]);
setDisallowedTools([]);
setSkipPermissions(false);
+ setProjectSortOrder('name');
}
} catch (error) {
console.error('Error loading tool settings:', error);
@@ -63,6 +66,7 @@ function ToolsSettings({ isOpen, onClose }) {
setAllowedTools([]);
setDisallowedTools([]);
setSkipPermissions(false);
+ setProjectSortOrder('name');
}
};
@@ -75,6 +79,7 @@ function ToolsSettings({ isOpen, onClose }) {
allowedTools,
disallowedTools,
skipPermissions,
+ projectSortOrder,
lastUpdated: new Date().toISOString()
};
@@ -181,6 +186,28 @@ function ToolsSettings({ isOpen, onClose }) {
+
+ {/* Project Sorting - Moved under Appearance */}
+
+
+
+
+ Project Sorting
+
+
+ How projects are ordered in the sidebar
+
+
+
setProjectSortOrder(e.target.value)}
+ className="text-sm bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 w-32"
+ >
+ Alphabetical
+ Recent Activity
+
+
+
@@ -418,4 +445,4 @@ function ToolsSettings({ isOpen, onClose }) {
);
}
-export default ToolsSettings;
\ No newline at end of file
+export default ToolsSettings;