diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index 1b83ae5..a4cffd5 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -21,6 +21,7 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) { const [sessionTitle, setSessionTitle] = useState(''); const [sessionModel, setSessionModel] = useState(''); const [sessionMode, setSessionMode] = useState(''); + const [projectName, setProjectName] = useState(''); const { setWorkingDirectory, setSessionId, setSessionTitle: setPanelSessionTitle, setPanelOpen } = usePanel(); // Load session info and set working directory @@ -32,6 +33,7 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) { const data: { session: ChatSession } = await res.json(); if (data.session.working_directory) { setWorkingDirectory(data.session.working_directory); + localStorage.setItem("codepilot:last-working-directory", data.session.working_directory); } setSessionId(id); setPanelOpen(true); @@ -40,6 +42,7 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) { setPanelSessionTitle(title); setSessionModel(data.session.model || ''); setSessionMode(data.session.mode || 'code'); + setProjectName(data.session.project_name || ''); } } catch { // Session info load failed - panel will still work without directory @@ -112,9 +115,15 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) { {/* Chat title bar */} {sessionTitle && (
+ {projectName && ( + <> + {projectName} + / + + )}

{sessionTitle}

diff --git a/src/components/layout/ChatListPanel.tsx b/src/components/layout/ChatListPanel.tsx index 314dc0a..bc356a9 100644 --- a/src/components/layout/ChatListPanel.tsx +++ b/src/components/layout/ChatListPanel.tsx @@ -2,9 +2,19 @@ import { usePathname, useRouter } from "next/navigation"; import Link from "next/link"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, useMemo } from "react"; import { HugeiconsIcon } from "@hugeicons/react"; -import { Delete02Icon, Search01Icon, Notification02Icon, FileImportIcon } from "@hugeicons/core-free-icons"; +import { + Delete02Icon, + Search01Icon, + Notification02Icon, + FileImportIcon, + Folder01Icon, + ArrowDown01Icon, + ArrowRight01Icon, + PlusSignIcon, + FolderOpenIcon, +} from "@hugeicons/core-free-icons"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; @@ -17,6 +27,7 @@ import { cn } from "@/lib/utils"; import { usePanel } from "@/hooks/usePanel"; import { ConnectionStatus } from "./ConnectionStatus"; import { ImportSessionDialog } from "./ImportSessionDialog"; +import { FolderPicker } from "@/components/chat/FolderPicker"; import type { ChatSession } from "@/types"; interface ChatListPanelProps { @@ -33,34 +44,65 @@ function formatRelativeTime(dateStr: string): string { const diffDay = Math.floor(diffHr / 24); if (diffMin < 1) return "just now"; - if (diffMin < 60) return `${diffMin}m ago`; - if (diffHr < 24) return `${diffHr}h ago`; - if (diffDay < 7) return `${diffDay}d ago`; + if (diffMin < 60) return `${diffMin}m`; + if (diffHr < 24) return `${diffHr}h`; + if (diffDay < 7) return `${diffDay}d`; return date.toLocaleDateString(); } -const DATE_GROUP_ORDER = ["Today", "Yesterday", "Last 7 Days", "Older"]; +const COLLAPSED_PROJECTS_KEY = "codepilot:collapsed-projects"; -function groupSessionsByDate( - sessions: ChatSession[] -): Record { - const groups: Record = {}; - const now = new Date(); - const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const yesterday = new Date(today.getTime() - 86400000); - const lastWeek = new Date(today.getTime() - 7 * 86400000); - - for (const session of sessions) { - const date = new Date(session.updated_at); - let group: string; - if (date >= today) group = "Today"; - else if (date >= yesterday) group = "Yesterday"; - else if (date >= lastWeek) group = "Last 7 Days"; - else group = "Older"; - - if (!groups[group]) groups[group] = []; - groups[group].push(session); +function loadCollapsedProjects(): Set { + try { + const raw = localStorage.getItem(COLLAPSED_PROJECTS_KEY); + if (raw) return new Set(JSON.parse(raw)); + } catch { + // ignore } + return new Set(); +} + +function saveCollapsedProjects(collapsed: Set) { + localStorage.setItem(COLLAPSED_PROJECTS_KEY, JSON.stringify([...collapsed])); +} + +interface ProjectGroup { + workingDirectory: string; + displayName: string; + sessions: ChatSession[]; + latestUpdatedAt: number; +} + +function groupSessionsByProject(sessions: ChatSession[]): ProjectGroup[] { + const map = new Map(); + for (const session of sessions) { + const key = session.working_directory || ""; + if (!map.has(key)) map.set(key, []); + map.get(key)!.push(session); + } + + const groups: ProjectGroup[] = []; + for (const [wd, groupSessions] of map) { + // Sort sessions within group by updated_at DESC + groupSessions.sort( + (a, b) => + new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() + ); + const displayName = + wd === "" + ? "No Project" + : groupSessions[0]?.project_name || wd.split("/").pop() || wd; + const latestUpdatedAt = new Date(groupSessions[0].updated_at).getTime(); + groups.push({ + workingDirectory: wd, + displayName, + sessions: groupSessions, + latestUpdatedAt, + }); + } + + // Sort groups by most recently active first + groups.sort((a, b) => b.latestUpdatedAt - a.latestUpdatedAt); return groups; } @@ -79,6 +121,21 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { const [deletingSession, setDeletingSession] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [importDialogOpen, setImportDialogOpen] = useState(false); + const [folderPickerOpen, setFolderPickerOpen] = useState(false); + const [collapsedProjects, setCollapsedProjects] = useState>( + () => loadCollapsedProjects() + ); + const [hoveredFolder, setHoveredFolder] = useState(null); + + const toggleProject = useCallback((wd: string) => { + setCollapsedProjects((prev) => { + const next = new Set(prev); + if (next.has(wd)) next.delete(wd); + else next.add(wd); + saveCollapsedProjects(next); + return next; + }); + }, []); const fetchSessions = useCallback(async () => { try { @@ -104,11 +161,11 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { // Refresh session list when a session is created or updated useEffect(() => { const handler = () => fetchSessions(); - window.addEventListener('session-created', handler); - window.addEventListener('session-updated', handler); + window.addEventListener("session-created", handler); + window.addEventListener("session-updated", handler); return () => { - window.removeEventListener('session-created', handler); - window.removeEventListener('session-updated', handler); + window.removeEventListener("session-created", handler); + window.removeEventListener("session-updated", handler); }; }, [fetchSessions]); @@ -118,7 +175,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { ) => { e.preventDefault(); e.stopPropagation(); - if (!confirm('Delete this conversation?')) return; + if (!confirm("Delete this conversation?")) return; setDeletingSession(sessionId); try { const res = await fetch(`/api/chat/sessions/${sessionId}`, { @@ -137,6 +194,46 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { } }; + const handleCreateSessionInProject = async ( + e: React.MouseEvent, + workingDirectory: string + ) => { + e.stopPropagation(); + try { + const res = await fetch("/api/chat/sessions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ working_directory: workingDirectory }), + }); + if (res.ok) { + const data = await res.json(); + window.dispatchEvent(new CustomEvent("session-created")); + router.push(`/chat/${data.session.id}`); + } + } catch { + // Silently fail + } + }; + + const handleFolderSelect = async (path: string) => { + try { + const res = await fetch("/api/chat/sessions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ working_directory: path }), + }); + if (res.ok) { + const data = await res.json(); + window.dispatchEvent(new CustomEvent("session-created")); + router.push(`/chat/${data.session.id}`); + } + } catch { + // Silently fail + } + }; + + const isSearching = searchQuery.length > 0; + const filteredSessions = searchQuery ? sessions.filter( (s) => @@ -146,26 +243,51 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { ) : sessions; - const groupedSessions = groupSessionsByDate(filteredSessions); + const projectGroups = useMemo( + () => groupSessionsByProject(filteredSessions), + [filteredSessions] + ); if (!open) return null; return ( -