diff --git a/src-tauri/src/commands/claude.rs b/src-tauri/src/commands/claude.rs index 1ccee31..b020e6e 100644 --- a/src-tauri/src/commands/claude.rs +++ b/src-tauri/src/commands/claude.rs @@ -35,6 +35,8 @@ pub struct Project { pub sessions: Vec, /// Unix timestamp when the project directory was created pub created_at: u64, + /// Unix timestamp of the most recent session (last modified time of newest JSONL file) + pub last_session_time: u64, } /// Represents a session with its metadata @@ -422,6 +424,8 @@ pub async fn list_projects() -> Result, String> { // List all JSONL files (sessions) in this project directory let mut sessions = Vec::new(); + let mut last_session_time = created_at; // Default to project creation time + if let Ok(session_entries) = fs::read_dir(&path) { for session_entry in session_entries.flatten() { let session_path = session_entry.path(); @@ -431,6 +435,21 @@ pub async fn list_projects() -> Result, String> { if let Some(session_id) = session_path.file_stem().and_then(|s| s.to_str()) { sessions.push(session_id.to_string()); + + // Get the modified time of this session file + if let Ok(metadata) = fs::metadata(&session_path) { + if let Ok(modified) = metadata.modified() { + let modified_time = modified + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + // Update last_session_time if this file is newer + if modified_time > last_session_time { + last_session_time = modified_time; + } + } + } } } } @@ -441,12 +460,13 @@ pub async fn list_projects() -> Result, String> { path: project_path, sessions, created_at, + last_session_time, }); } } - // Sort projects by creation time (newest first) - projects.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + // Sort projects by last session time (newest first) + projects.sort_by(|a, b| b.last_session_time.cmp(&a.last_session_time)); log::info!("Found {} projects", projects.len()); Ok(projects) diff --git a/src/components/ProjectList.tsx b/src/components/ProjectList.tsx index f34657d..5c5141c 100644 --- a/src/components/ProjectList.tsx +++ b/src/components/ProjectList.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useMemo } from "react"; import { motion } from "framer-motion"; import { FolderOpen, @@ -6,11 +6,14 @@ import { FileText, ChevronRight, Settings, - MoreVertical + MoreVertical, + Search, + X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; import { DropdownMenu, DropdownMenuContent, @@ -73,20 +76,96 @@ export const ProjectList: React.FC = ({ }) => { const { t } = useTranslation(); const [currentPage, setCurrentPage] = useState(1); + const [searchQuery, setSearchQuery] = useState(""); + + // Sort and filter projects + const filteredAndSortedProjects = useMemo(() => { + // First, sort by last_session_time in descending order (newest first) + let sorted = [...projects].sort((a, b) => b.last_session_time - a.last_session_time); + + // Then filter by search query + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + sorted = sorted.filter(project => + getProjectName(project.path).toLowerCase().includes(query) + ); + } + + return sorted; + }, [projects, searchQuery]); // Calculate pagination - const totalPages = Math.ceil(projects.length / ITEMS_PER_PAGE); + const totalPages = Math.ceil(filteredAndSortedProjects.length / ITEMS_PER_PAGE); const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; const endIndex = startIndex + ITEMS_PER_PAGE; - const currentProjects = projects.slice(startIndex, endIndex); + const currentProjects = filteredAndSortedProjects.slice(startIndex, endIndex); - // Reset to page 1 if projects change + // Reset to page 1 if projects or search query changes React.useEffect(() => { setCurrentPage(1); - }, [projects.length]); + }, [projects.length, searchQuery]); return (
+ {/* Search bar and results info */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-9 pr-9" + /> + {searchQuery && ( + + )} +
+ + {/* Results info */} +
+ {searchQuery ? ( + + {t('showingResults')}: {filteredAndSortedProjects.length} / {projects.length} + + ) : ( + + {t('totalProjects')}: {projects.length} + + )} +
+
+ + {/* Empty state */} + {filteredAndSortedProjects.length === 0 && searchQuery && ( + + +

{t('noSearchResults')}

+

+ {t('noProjectsMatchSearch')} "{searchQuery}" +

+ +
+ )} + + {/* Project grid */}
{currentProjects.map((project, index) => ( = ({ }} > onProjectClick(project)} > -
+ {/* Hover gradient effect */} +
+
-
+
- -

+
+ +
+

{getProjectName(project.path)}

{project.sessions.length > 0 && ( - + {project.sessions.length} )}
-

+

{project.path}

diff --git a/src/lib/api.ts b/src/lib/api.ts index 9576512..82d7d07 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -29,6 +29,8 @@ export interface Project { sessions: string[]; /** Unix timestamp when the project directory was created */ created_at: number; + /** Unix timestamp of the most recent session (last modified time of newest JSONL file) */ + last_session_time: number; } /** diff --git a/src/locales/en/common.json b/src/locales/en/common.json index a5a878e..0025d0c 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -697,6 +697,12 @@ "searchedLocations": "Searched locations", "installationTip": "You can install Claude Code using", "searchingInstallations": "Searching for Claude installations...", + "searchProjects": "Search projects...", + "showingResults": "Showing results", + "totalProjects": "Total projects", + "noSearchResults": "No search results", + "noProjectsMatchSearch": "No projects match your search for", + "clearSearch": "Clear search", "installationGuide": "Installation Guide", "validating": "Validating...", "saveSelection": "Save Selection", diff --git a/src/locales/zh/common.json b/src/locales/zh/common.json index cb781b8..602f5af 100644 --- a/src/locales/zh/common.json +++ b/src/locales/zh/common.json @@ -597,7 +597,6 @@ "hourlyUsageToday": "24小时使用模式", "last24HoursPattern": "过去24小时使用模式", "noUsageData": "选定时期内无用量数据", - "totalProjects": "项目总数", "avgProjectCost": "平均项目成本", "topProjectCost": "最高项目成本", "projectCostDistribution": "项目成本分布", @@ -632,6 +631,12 @@ "claudeFileEditorNotImplemented": "标签页中的Claude文件编辑器尚未实现", "noAgentDataSpecified": "未指定智能体数据", "importAgentComingSoon": "导入智能体功能即将推出...", + "searchProjects": "搜索项目...", + "showingResults": "显示结果", + "totalProjects": "总项目数", + "noSearchResults": "未找到搜索结果", + "noProjectsMatchSearch": "没有项目匹配搜索", + "clearSearch": "清除搜索", "unknownTabType": "未知的标签页类型", "typeYourPromptHere": "在此输入您的提示...", "dropImagesHere": "在此放置图片...",