Merge branch 'main' into feature/project-starring

This commit is contained in:
viper151
2025-07-12 20:41:08 +02:00
committed by GitHub
4 changed files with 339 additions and 5 deletions

View File

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

View File

@@ -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) => (
<div key={item.path} className="select-none">
@@ -135,6 +173,129 @@ function FileTree({ selectedProject }) {
}
};
// Render detailed view with table-like layout
const renderDetailedView = (items, level = 0) => {
return items.map((item) => (
<div key={item.path} className="select-none">
<div
className={cn(
"grid grid-cols-12 gap-2 p-2 hover:bg-accent cursor-pointer items-center",
)}
style={{ paddingLeft: `${level * 16 + 12}px` }}
onClick={() => {
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
});
}
}}
>
<div className="col-span-5 flex items-center gap-2 min-w-0">
{item.type === 'directory' ? (
expandedDirs.has(item.path) ? (
<FolderOpen className="w-4 h-4 text-blue-500 flex-shrink-0" />
) : (
<Folder className="w-4 h-4 text-muted-foreground flex-shrink-0" />
)
) : (
getFileIcon(item.name)
)}
<span className="text-sm truncate text-foreground">
{item.name}
</span>
</div>
<div className="col-span-2 text-sm text-muted-foreground">
{item.type === 'file' ? formatFileSize(item.size) : '-'}
</div>
<div className="col-span-3 text-sm text-muted-foreground">
{formatRelativeTime(item.modified)}
</div>
<div className="col-span-2 text-sm text-muted-foreground font-mono">
{item.permissionsRwx || '-'}
</div>
</div>
{item.type === 'directory' &&
expandedDirs.has(item.path) &&
item.children &&
renderDetailedView(item.children, level + 1)}
</div>
));
};
// Render compact view with inline details
const renderCompactView = (items, level = 0) => {
return items.map((item) => (
<div key={item.path} className="select-none">
<div
className={cn(
"flex items-center justify-between p-2 hover:bg-accent cursor-pointer",
)}
style={{ paddingLeft: `${level * 16 + 12}px` }}
onClick={() => {
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
});
}
}}
>
<div className="flex items-center gap-2 min-w-0">
{item.type === 'directory' ? (
expandedDirs.has(item.path) ? (
<FolderOpen className="w-4 h-4 text-blue-500 flex-shrink-0" />
) : (
<Folder className="w-4 h-4 text-muted-foreground flex-shrink-0" />
)
) : (
getFileIcon(item.name)
)}
<span className="text-sm truncate text-foreground">
{item.name}
</span>
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
{item.type === 'file' && (
<>
<span>{formatFileSize(item.size)}</span>
<span className="font-mono">{item.permissionsRwx}</span>
</>
)}
</div>
</div>
{item.type === 'directory' &&
expandedDirs.has(item.path) &&
item.children &&
renderCompactView(item.children, level + 1)}
</div>
));
};
if (loading) {
return (
<div className="h-full flex items-center justify-center">
@@ -147,6 +308,51 @@ function FileTree({ selectedProject }) {
return (
<div className="h-full flex flex-col bg-card">
{/* View Mode Toggle */}
<div className="p-4 border-b border-border flex items-center justify-between">
<h3 className="text-sm font-medium text-foreground">Files</h3>
<div className="flex gap-1">
<Button
variant={viewMode === 'simple' ? 'default' : 'ghost'}
size="sm"
className="h-8 w-8 p-0"
onClick={() => changeViewMode('simple')}
title="Simple view"
>
<List className="w-4 h-4" />
</Button>
<Button
variant={viewMode === 'compact' ? 'default' : 'ghost'}
size="sm"
className="h-8 w-8 p-0"
onClick={() => changeViewMode('compact')}
title="Compact view"
>
<Eye className="w-4 h-4" />
</Button>
<Button
variant={viewMode === 'detailed' ? 'default' : 'ghost'}
size="sm"
className="h-8 w-8 p-0"
onClick={() => changeViewMode('detailed')}
title="Detailed view"
>
<TableProperties className="w-4 h-4" />
</Button>
</div>
</div>
{/* Column Headers for Detailed View */}
{viewMode === 'detailed' && files.length > 0 && (
<div className="px-4 pt-2 pb-1 border-b border-border">
<div className="grid grid-cols-12 gap-2 px-2 text-xs font-medium text-muted-foreground">
<div className="col-span-5">Name</div>
<div className="col-span-2">Size</div>
<div className="col-span-3">Modified</div>
<div className="col-span-2">Permissions</div>
</div>
</div>
)}
<ScrollArea className="flex-1 p-4">
{files.length === 0 ? (
@@ -160,8 +366,10 @@ function FileTree({ selectedProject }) {
</p>
</div>
) : (
<div className="space-y-1">
{renderFileTree(files)}
<div className={viewMode === 'detailed' ? '' : 'space-y-1'}>
{viewMode === 'simple' && renderFileTree(files)}
{viewMode === 'compact' && renderCompactView(files)}
{viewMode === 'detailed' && renderDetailedView(files)}
</div>
)}
</ScrollArea>

View File

@@ -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 (
<div className="h-full flex flex-col bg-card md:select-none">
{/* Header */}

View File

@@ -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 }) {
</span>
</button>
</div>
{/* Project Sorting - Moved under Appearance */}
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
Project Sorting
</div>
<div className="text-sm text-muted-foreground">
How projects are ordered in the sidebar
</div>
</div>
<select
value={projectSortOrder}
onChange={(e) => 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"
>
<option value="name">Alphabetical</option>
<option value="date">Recent Activity</option>
</select>
</div>
</div>
</div>
</div>
@@ -418,4 +445,4 @@ function ToolsSettings({ isOpen, onClose }) {
);
}
export default ToolsSettings;
export default ToolsSettings;