增加claude code 项目查询
优化历史记录排序
This commit is contained in:
@@ -35,6 +35,8 @@ pub struct Project {
|
|||||||
pub sessions: Vec<String>,
|
pub sessions: Vec<String>,
|
||||||
/// Unix timestamp when the project directory was created
|
/// Unix timestamp when the project directory was created
|
||||||
pub created_at: u64,
|
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
|
/// Represents a session with its metadata
|
||||||
@@ -422,6 +424,8 @@ pub async fn list_projects() -> Result<Vec<Project>, String> {
|
|||||||
|
|
||||||
// List all JSONL files (sessions) in this project directory
|
// List all JSONL files (sessions) in this project directory
|
||||||
let mut sessions = Vec::new();
|
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) {
|
if let Ok(session_entries) = fs::read_dir(&path) {
|
||||||
for session_entry in session_entries.flatten() {
|
for session_entry in session_entries.flatten() {
|
||||||
let session_path = session_entry.path();
|
let session_path = session_entry.path();
|
||||||
@@ -431,6 +435,21 @@ pub async fn list_projects() -> Result<Vec<Project>, String> {
|
|||||||
if let Some(session_id) = session_path.file_stem().and_then(|s| s.to_str())
|
if let Some(session_id) = session_path.file_stem().and_then(|s| s.to_str())
|
||||||
{
|
{
|
||||||
sessions.push(session_id.to_string());
|
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<Vec<Project>, String> {
|
|||||||
path: project_path,
|
path: project_path,
|
||||||
sessions,
|
sessions,
|
||||||
created_at,
|
created_at,
|
||||||
|
last_session_time,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort projects by creation time (newest first)
|
// Sort projects by last session time (newest first)
|
||||||
projects.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
projects.sort_by(|a, b| b.last_session_time.cmp(&a.last_session_time));
|
||||||
|
|
||||||
log::info!("Found {} projects", projects.len());
|
log::info!("Found {} projects", projects.len());
|
||||||
Ok(projects)
|
Ok(projects)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useMemo } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import {
|
import {
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
@@ -6,11 +6,14 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Settings,
|
Settings,
|
||||||
MoreVertical
|
MoreVertical,
|
||||||
|
Search,
|
||||||
|
X
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -73,20 +76,96 @@ export const ProjectList: React.FC<ProjectListProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
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
|
// 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 startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
||||||
const endIndex = startIndex + 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(() => {
|
React.useEffect(() => {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}, [projects.length]);
|
}, [projects.length, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-4", className)}>
|
<div className={cn("space-y-4", className)}>
|
||||||
|
{/* Search bar and results info */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||||
|
<div className="relative flex-1 max-w-md">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={t('searchProjects')}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-9 pr-9"
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSearchQuery("")}
|
||||||
|
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-7 w-7 p-0 hover:bg-muted"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results info */}
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{searchQuery ? (
|
||||||
|
<span>
|
||||||
|
{t('showingResults')}: <span className="font-semibold text-foreground">{filteredAndSortedProjects.length}</span> / {projects.length}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
{t('totalProjects')}: <span className="font-semibold text-foreground">{projects.length}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Empty state */}
|
||||||
|
{filteredAndSortedProjects.length === 0 && searchQuery && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="text-center py-12"
|
||||||
|
>
|
||||||
|
<Search className="h-12 w-12 text-muted-foreground mx-auto mb-4 opacity-50" />
|
||||||
|
<h3 className="text-lg font-semibold mb-2">{t('noSearchResults')}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
{t('noProjectsMatchSearch')} "{searchQuery}"
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setSearchQuery("")}
|
||||||
|
>
|
||||||
|
{t('clearSearch')}
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Project grid */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||||
{currentProjects.map((project, index) => (
|
{currentProjects.map((project, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -100,26 +179,30 @@ export const ProjectList: React.FC<ProjectListProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
className="p-4 hover:shadow-md transition-all duration-200 cursor-pointer group h-full"
|
className="p-4 hover:shadow-lg hover:border-primary/50 transition-all duration-200 cursor-pointer group h-full relative overflow-hidden"
|
||||||
onClick={() => onProjectClick(project)}
|
onClick={() => onProjectClick(project)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col h-full">
|
{/* Hover gradient effect */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-200" />
|
||||||
|
<div className="flex flex-col h-full relative z-10">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-start justify-between mb-2">
|
<div className="flex items-start justify-between mb-3">
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
<FolderOpen className="h-5 w-5 text-primary shrink-0" />
|
<div className="p-2 rounded-lg bg-primary/10 group-hover:bg-primary/20 transition-colors">
|
||||||
<h3 className="font-semibold text-base truncate">
|
<FolderOpen className="h-4 w-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-base truncate group-hover:text-primary transition-colors">
|
||||||
{getProjectName(project.path)}
|
{getProjectName(project.path)}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
{project.sessions.length > 0 && (
|
{project.sessions.length > 0 && (
|
||||||
<Badge variant="secondary" className="shrink-0 ml-2">
|
<Badge variant="secondary" className="shrink-0 ml-2 group-hover:bg-primary group-hover:text-primary-foreground transition-colors">
|
||||||
{project.sessions.length}
|
{project.sessions.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground mb-3 font-mono truncate">
|
<p className="text-xs text-muted-foreground mb-4 font-mono truncate bg-muted/50 rounded px-2 py-1">
|
||||||
{project.path}
|
{project.path}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export interface Project {
|
|||||||
sessions: string[];
|
sessions: string[];
|
||||||
/** Unix timestamp when the project directory was created */
|
/** Unix timestamp when the project directory was created */
|
||||||
created_at: number;
|
created_at: number;
|
||||||
|
/** Unix timestamp of the most recent session (last modified time of newest JSONL file) */
|
||||||
|
last_session_time: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -697,6 +697,12 @@
|
|||||||
"searchedLocations": "Searched locations",
|
"searchedLocations": "Searched locations",
|
||||||
"installationTip": "You can install Claude Code using",
|
"installationTip": "You can install Claude Code using",
|
||||||
"searchingInstallations": "Searching for Claude installations...",
|
"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",
|
"installationGuide": "Installation Guide",
|
||||||
"validating": "Validating...",
|
"validating": "Validating...",
|
||||||
"saveSelection": "Save Selection",
|
"saveSelection": "Save Selection",
|
||||||
|
|||||||
@@ -597,7 +597,6 @@
|
|||||||
"hourlyUsageToday": "24小时使用模式",
|
"hourlyUsageToday": "24小时使用模式",
|
||||||
"last24HoursPattern": "过去24小时使用模式",
|
"last24HoursPattern": "过去24小时使用模式",
|
||||||
"noUsageData": "选定时期内无用量数据",
|
"noUsageData": "选定时期内无用量数据",
|
||||||
"totalProjects": "项目总数",
|
|
||||||
"avgProjectCost": "平均项目成本",
|
"avgProjectCost": "平均项目成本",
|
||||||
"topProjectCost": "最高项目成本",
|
"topProjectCost": "最高项目成本",
|
||||||
"projectCostDistribution": "项目成本分布",
|
"projectCostDistribution": "项目成本分布",
|
||||||
@@ -632,6 +631,12 @@
|
|||||||
"claudeFileEditorNotImplemented": "标签页中的Claude文件编辑器尚未实现",
|
"claudeFileEditorNotImplemented": "标签页中的Claude文件编辑器尚未实现",
|
||||||
"noAgentDataSpecified": "未指定智能体数据",
|
"noAgentDataSpecified": "未指定智能体数据",
|
||||||
"importAgentComingSoon": "导入智能体功能即将推出...",
|
"importAgentComingSoon": "导入智能体功能即将推出...",
|
||||||
|
"searchProjects": "搜索项目...",
|
||||||
|
"showingResults": "显示结果",
|
||||||
|
"totalProjects": "总项目数",
|
||||||
|
"noSearchResults": "未找到搜索结果",
|
||||||
|
"noProjectsMatchSearch": "没有项目匹配搜索",
|
||||||
|
"clearSearch": "清除搜索",
|
||||||
"unknownTabType": "未知的标签页类型",
|
"unknownTabType": "未知的标签页类型",
|
||||||
"typeYourPromptHere": "在此输入您的提示...",
|
"typeYourPromptHere": "在此输入您的提示...",
|
||||||
"dropImagesHere": "在此放置图片...",
|
"dropImagesHere": "在此放置图片...",
|
||||||
|
|||||||
Reference in New Issue
Block a user