refactor: Restructure files and folders to better mimic feature-based architecture

This commit is contained in:
Haileyesus
2026-02-11 18:11:06 +03:00
parent fadbcc8259
commit 7f45540dbe
34 changed files with 124 additions and 233 deletions

View File

@@ -0,0 +1,59 @@
import { Settings, Sparkles } from 'lucide-react';
import type { TFunction } from 'i18next';
type SidebarCollapsedProps = {
onExpand: () => void;
onShowSettings: () => void;
updateAvailable: boolean;
onShowVersionModal: () => void;
t: TFunction;
};
export default function SidebarCollapsed({
onExpand,
onShowSettings,
updateAvailable,
onShowVersionModal,
t,
}: SidebarCollapsedProps) {
return (
<div className="h-full flex flex-col items-center py-4 gap-4 bg-card">
<button
onClick={onExpand}
className="p-2 hover:bg-accent rounded-md transition-colors duration-200 group"
aria-label={t('common:versionUpdate.ariaLabels.showSidebar')}
title={t('common:versionUpdate.ariaLabels.showSidebar')}
>
<svg
className="w-5 h-5 text-foreground group-hover:scale-110 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg>
</button>
<button
onClick={onShowSettings}
className="p-2 hover:bg-accent rounded-md transition-colors duration-200"
aria-label={t('actions.settings')}
title={t('actions.settings')}
>
<Settings className="w-5 h-5 text-muted-foreground hover:text-foreground transition-colors" />
</button>
{updateAvailable && (
<button
onClick={onShowVersionModal}
className="relative p-2 hover:bg-accent rounded-md transition-colors duration-200"
aria-label={t('common:versionUpdate.ariaLabels.updateAvailable')}
title={t('common:versionUpdate.ariaLabels.updateAvailable')}
>
<Sparkles className="w-5 h-5 text-blue-500" />
<span className="absolute top-1 right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
</button>
)}
</div>
);
}

View File

@@ -0,0 +1,84 @@
import { ScrollArea } from '../../../ui/scroll-area';
import type { TFunction } from 'i18next';
import type { Project } from '../../../../types/app';
import type { ReleaseInfo } from '../../../../types/sharedTypes';
import SidebarFooter from './SidebarFooter';
import SidebarHeader from './SidebarHeader';
import SidebarProjectList, { type SidebarProjectListProps } from './SidebarProjectList';
type SidebarContentProps = {
isPWA: boolean;
isMobile: boolean;
isLoading: boolean;
projects: Project[];
searchFilter: string;
onSearchFilterChange: (value: string) => void;
onClearSearchFilter: () => void;
onRefresh: () => void;
isRefreshing: boolean;
onCreateProject: () => void;
onCollapseSidebar: () => void;
updateAvailable: boolean;
releaseInfo: ReleaseInfo | null;
latestVersion: string | null;
onShowVersionModal: () => void;
onShowSettings: () => void;
projectListProps: SidebarProjectListProps;
t: TFunction;
};
export default function SidebarContent({
isPWA,
isMobile,
isLoading,
projects,
searchFilter,
onSearchFilterChange,
onClearSearchFilter,
onRefresh,
isRefreshing,
onCreateProject,
onCollapseSidebar,
updateAvailable,
releaseInfo,
latestVersion,
onShowVersionModal,
onShowSettings,
projectListProps,
t,
}: SidebarContentProps) {
return (
<div
className="h-full flex flex-col bg-card md:select-none md:w-80"
style={isPWA && isMobile ? { paddingTop: '44px' } : {}}
>
<SidebarHeader
isPWA={isPWA}
isMobile={isMobile}
isLoading={isLoading}
projectsCount={projects.length}
searchFilter={searchFilter}
onSearchFilterChange={onSearchFilterChange}
onClearSearchFilter={onClearSearchFilter}
onRefresh={onRefresh}
isRefreshing={isRefreshing}
onCreateProject={onCreateProject}
onCollapseSidebar={onCollapseSidebar}
t={t}
/>
<ScrollArea className="flex-1 md:px-2 md:py-3 overflow-y-auto overscroll-contain">
<SidebarProjectList {...projectListProps} />
</ScrollArea>
<SidebarFooter
updateAvailable={updateAvailable}
releaseInfo={releaseInfo}
latestVersion={latestVersion}
onShowVersionModal={onShowVersionModal}
onShowSettings={onShowSettings}
t={t}
/>
</div>
);
}

View File

@@ -0,0 +1,94 @@
import { Settings } from 'lucide-react';
import type { TFunction } from 'i18next';
import type { ReleaseInfo } from '../../../../types/sharedTypes';
import { Button } from '../../../ui/button';
type SidebarFooterProps = {
updateAvailable: boolean;
releaseInfo: ReleaseInfo | null;
latestVersion: string | null;
onShowVersionModal: () => void;
onShowSettings: () => void;
t: TFunction;
};
export default function SidebarFooter({
updateAvailable,
releaseInfo,
latestVersion,
onShowVersionModal,
onShowSettings,
t,
}: SidebarFooterProps) {
return (
<>
{updateAvailable && (
<div className="md:p-2 border-t border-border/50 flex-shrink-0">
<div className="hidden md:block">
<Button
variant="ghost"
className="w-full justify-start gap-3 p-3 h-auto font-normal text-left hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors duration-200 border border-blue-200 dark:border-blue-700 rounded-lg mb-2"
onClick={onShowVersionModal}
>
<div className="relative">
<svg className="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
</svg>
<div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">
{releaseInfo?.title || `Version ${latestVersion}`}
</div>
<div className="text-xs text-blue-600 dark:text-blue-400">{t('version.updateAvailable')}</div>
</div>
</Button>
</div>
<div className="md:hidden p-3 pb-2">
<button
className="w-full h-12 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700 rounded-xl flex items-center justify-start gap-3 px-4 active:scale-[0.98] transition-all duration-150"
onClick={onShowVersionModal}
>
<div className="relative">
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
</svg>
<div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
</div>
<div className="min-w-0 flex-1 text-left">
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">
{releaseInfo?.title || `Version ${latestVersion}`}
</div>
<div className="text-xs text-blue-600 dark:text-blue-400">{t('version.updateAvailable')}</div>
</div>
</button>
</div>
</div>
)}
<div className="md:p-2 md:border-t md:border-border flex-shrink-0">
<div className="md:hidden p-4 pb-20 border-t border-border/50">
<button
className="w-full h-14 bg-muted/50 hover:bg-muted/70 rounded-2xl flex items-center justify-start gap-4 px-4 active:scale-[0.98] transition-all duration-150"
onClick={onShowSettings}
>
<div className="w-10 h-10 rounded-2xl bg-background/80 flex items-center justify-center">
<Settings className="w-5 h-5 text-muted-foreground" />
</div>
<span className="text-lg font-medium text-foreground">{t('actions.settings')}</span>
</button>
</div>
<Button
variant="ghost"
className="hidden md:flex w-full justify-start gap-2 p-2 h-auto font-normal text-muted-foreground hover:text-foreground hover:bg-accent transition-colors duration-200"
onClick={onShowSettings}
>
<Settings className="w-3 h-3" />
<span className="text-xs">{t('actions.settings')}</span>
</Button>
</div>
</>
);
}

View File

@@ -0,0 +1,187 @@
import { FolderPlus, MessageSquare, RefreshCw, Search, X } from 'lucide-react';
import type { TFunction } from 'i18next';
import { Button } from '../../../ui/button';
import { Input } from '../../../ui/input';
import { IS_PLATFORM } from '../../../../constants/config';
type SidebarHeaderProps = {
isPWA: boolean;
isMobile: boolean;
isLoading: boolean;
projectsCount: number;
searchFilter: string;
onSearchFilterChange: (value: string) => void;
onClearSearchFilter: () => void;
onRefresh: () => void;
isRefreshing: boolean;
onCreateProject: () => void;
onCollapseSidebar: () => void;
t: TFunction;
};
export default function SidebarHeader({
isPWA,
isMobile,
isLoading,
projectsCount,
searchFilter,
onSearchFilterChange,
onClearSearchFilter,
onRefresh,
isRefreshing,
onCreateProject,
onCollapseSidebar,
t,
}: SidebarHeaderProps) {
return (
<>
<div
className="md:p-4 md:border-b md:border-border"
style={isPWA && isMobile ? { paddingTop: '44px' } : {}}
>
<div className="hidden md:flex items-center justify-between">
{IS_PLATFORM ? (
<a
href="https://cloudcli.ai/dashboard"
className="flex items-center gap-3 hover:opacity-80 transition-opacity group"
title={t('tooltips.viewEnvironments')}
>
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center shadow-sm group-hover:shadow-md transition-shadow">
<MessageSquare className="w-4 h-4 text-primary-foreground" />
</div>
<div>
<h1 className="text-lg font-bold text-foreground">{t('app.title')}</h1>
<p className="text-sm text-muted-foreground">{t('app.subtitle')}</p>
</div>
</a>
) : (
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center shadow-sm">
<MessageSquare className="w-4 h-4 text-primary-foreground" />
</div>
<div>
<h1 className="text-lg font-bold text-foreground">{t('app.title')}</h1>
<p className="text-sm text-muted-foreground">{t('app.subtitle')}</p>
</div>
</div>
)}
<Button
variant="ghost"
size="sm"
className="h-8 w-8 px-0 hover:bg-accent transition-colors duration-200"
onClick={onCollapseSidebar}
title={t('tooltips.hideSidebar')}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Button>
</div>
<div
className="md:hidden p-3 border-b border-border"
style={isPWA && isMobile ? { paddingTop: '16px' } : {}}
>
<div className="flex items-center justify-between">
{IS_PLATFORM ? (
<a
href="https://cloudcli.ai/dashboard"
className="flex items-center gap-3 active:opacity-70 transition-opacity"
title={t('tooltips.viewEnvironments')}
>
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
<MessageSquare className="w-4 h-4 text-primary-foreground" />
</div>
<div>
<h1 className="text-lg font-semibold text-foreground">{t('app.title')}</h1>
<p className="text-sm text-muted-foreground">{t('projects.title')}</p>
</div>
</a>
) : (
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
<MessageSquare className="w-4 h-4 text-primary-foreground" />
</div>
<div>
<h1 className="text-lg font-semibold text-foreground">{t('app.title')}</h1>
<p className="text-sm text-muted-foreground">{t('projects.title')}</p>
</div>
</div>
)}
<div className="flex gap-2">
<button
className="w-8 h-8 rounded-md bg-background border border-border flex items-center justify-center active:scale-95 transition-all duration-150"
onClick={onRefresh}
disabled={isRefreshing}
>
<RefreshCw className={`w-4 h-4 text-foreground ${isRefreshing ? 'animate-spin' : ''}`} />
</button>
<button
className="w-8 h-8 rounded-md bg-primary text-primary-foreground flex items-center justify-center active:scale-95 transition-all duration-150"
onClick={onCreateProject}
>
<FolderPlus className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
{!isLoading && !isMobile && (
<div className="px-3 md:px-4 py-2 border-b border-border">
<div className="flex gap-2">
<Button
variant="default"
size="sm"
className="flex-1 h-8 text-xs bg-primary hover:bg-primary/90 transition-all duration-200"
onClick={onCreateProject}
title={t('tooltips.createProject')}
>
<FolderPlus className="w-3.5 h-3.5 mr-1.5" />
{t('projects.newProject')}
</Button>
<Button
variant="outline"
size="sm"
className="h-8 w-8 px-0 hover:bg-accent transition-colors duration-200 group"
onClick={onRefresh}
disabled={isRefreshing}
title={t('tooltips.refresh')}
>
<RefreshCw
className={`w-3.5 h-3.5 ${
isRefreshing ? 'animate-spin' : 'group-hover:rotate-180 transition-transform duration-300'
}`}
/>
</Button>
</div>
</div>
)}
{projectsCount > 0 && !isLoading && (
<div className="px-3 md:px-4 py-2 border-b border-border">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type="text"
placeholder={t('projects.searchPlaceholder')}
value={searchFilter}
onChange={(event) => onSearchFilterChange(event.target.value)}
className="pl-9 h-9 text-sm bg-muted/50 border-0 focus:bg-background focus:ring-1 focus:ring-primary/20"
/>
{searchFilter && (
<button
onClick={onClearSearchFilter}
className="absolute right-2 top-1/2 transform -translate-y-1/2 p-1 hover:bg-accent rounded"
>
<X className="w-3 h-3 text-muted-foreground" />
</button>
)}
</div>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,180 @@
import ReactDOM from 'react-dom';
import { AlertTriangle, Trash2 } from 'lucide-react';
import type { TFunction } from 'i18next';
import { Button } from '../../../ui/button';
import ProjectCreationWizard from '../../../ProjectCreationWizard';
import Settings from '../../../Settings';
import VersionUpgradeModal from '../../../modals/VersionUpgradeModal';
import type { Project } from '../../../../types/app';
import type { ReleaseInfo } from '../../../../types/sharedTypes';
import type { DeleteProjectConfirmation, SessionDeleteConfirmation } from '../../types/types';
type SidebarModalsProps = {
projects: Project[];
showSettings: boolean;
settingsInitialTab: string;
onCloseSettings: () => void;
showNewProject: boolean;
onCloseNewProject: () => void;
onProjectCreated: () => void;
deleteConfirmation: DeleteProjectConfirmation | null;
onCancelDeleteProject: () => void;
onConfirmDeleteProject: () => void;
sessionDeleteConfirmation: SessionDeleteConfirmation | null;
onCancelDeleteSession: () => void;
onConfirmDeleteSession: () => void;
showVersionModal: boolean;
onCloseVersionModal: () => void;
releaseInfo: ReleaseInfo | null;
currentVersion: string;
latestVersion: string | null;
t: TFunction;
};
export default function SidebarModals({
projects,
showSettings,
settingsInitialTab,
onCloseSettings,
showNewProject,
onCloseNewProject,
onProjectCreated,
deleteConfirmation,
onCancelDeleteProject,
onConfirmDeleteProject,
sessionDeleteConfirmation,
onCancelDeleteSession,
onConfirmDeleteSession,
showVersionModal,
onCloseVersionModal,
releaseInfo,
currentVersion,
latestVersion,
t,
}: SidebarModalsProps) {
return (
<>
{showNewProject &&
ReactDOM.createPortal(
<ProjectCreationWizard
onClose={onCloseNewProject}
onProjectCreated={onProjectCreated}
/>,
document.body,
)}
<Settings
isOpen={showSettings}
onClose={onCloseSettings}
projects={projects as any}
initialTab={settingsInitialTab}
/>
{deleteConfirmation &&
ReactDOM.createPortal(
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-card border border-border rounded-xl shadow-2xl max-w-md w-full overflow-hidden">
<div className="p-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center flex-shrink-0">
<AlertTriangle className="w-6 h-6 text-red-600 dark:text-red-400" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-foreground mb-2">
{t('deleteConfirmation.deleteProject')}
</h3>
<p className="text-sm text-muted-foreground mb-1">
{t('deleteConfirmation.confirmDelete')}{' '}
<span className="font-medium text-foreground">
{deleteConfirmation.project.displayName || deleteConfirmation.project.name}
</span>
?
</p>
{deleteConfirmation.sessionCount > 0 && (
<div className="mt-3 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-sm text-red-700 dark:text-red-300 font-medium">
{t('deleteConfirmation.sessionCount', { count: deleteConfirmation.sessionCount })}
</p>
<p className="text-xs text-red-600 dark:text-red-400 mt-1">
{t('deleteConfirmation.allConversationsDeleted')}
</p>
</div>
)}
<p className="text-xs text-muted-foreground mt-3">
{t('deleteConfirmation.cannotUndo')}
</p>
</div>
</div>
</div>
<div className="flex gap-3 p-4 bg-muted/30 border-t border-border">
<Button variant="outline" className="flex-1" onClick={onCancelDeleteProject}>
{t('actions.cancel')}
</Button>
<Button
variant="destructive"
className="flex-1 bg-red-600 hover:bg-red-700 text-white"
onClick={onConfirmDeleteProject}
>
<Trash2 className="w-4 h-4 mr-2" />
{t('actions.delete')}
</Button>
</div>
</div>
</div>,
document.body,
)}
{sessionDeleteConfirmation &&
ReactDOM.createPortal(
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-card border border-border rounded-xl shadow-2xl max-w-md w-full overflow-hidden">
<div className="p-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center flex-shrink-0">
<AlertTriangle className="w-6 h-6 text-red-600 dark:text-red-400" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-foreground mb-2">
{t('deleteConfirmation.deleteSession')}
</h3>
<p className="text-sm text-muted-foreground mb-1">
{t('deleteConfirmation.confirmDelete')}{' '}
<span className="font-medium text-foreground">
{sessionDeleteConfirmation.sessionTitle || t('sessions.unnamed')}
</span>
?
</p>
<p className="text-xs text-muted-foreground mt-3">
{t('deleteConfirmation.cannotUndo')}
</p>
</div>
</div>
</div>
<div className="flex gap-3 p-4 bg-muted/30 border-t border-border">
<Button variant="outline" className="flex-1" onClick={onCancelDeleteSession}>
{t('actions.cancel')}
</Button>
<Button
variant="destructive"
className="flex-1 bg-red-600 hover:bg-red-700 text-white"
onClick={onConfirmDeleteSession}
>
<Trash2 className="w-4 h-4 mr-2" />
{t('actions.delete')}
</Button>
</div>
</div>
</div>,
document.body,
)}
<VersionUpgradeModal
isOpen={showVersionModal}
onClose={onCloseVersionModal}
releaseInfo={releaseInfo}
currentVersion={currentVersion}
latestVersion={latestVersion}
/>
</>
);
}

View File

@@ -0,0 +1,437 @@
import { Button } from '../../../ui/button';
import { Check, ChevronDown, ChevronRight, Edit3, Folder, FolderOpen, Star, Trash2, X } from 'lucide-react';
import type { TFunction } from 'i18next';
import { cn } from '../../../../lib/utils';
import TaskIndicator from '../../../TaskIndicator';
import type { Project, ProjectSession, SessionProvider } from '../../../../types/app';
import type { MCPServerStatus, SessionWithProvider, TouchHandlerFactory } from '../../types/types';
import { getTaskIndicatorStatus } from '../../utils/utils';
import SidebarProjectSessions from './SidebarProjectSessions';
type SidebarProjectItemProps = {
project: Project;
selectedProject: Project | null;
selectedSession: ProjectSession | null;
isExpanded: boolean;
isDeleting: boolean;
isStarred: boolean;
editingProject: string | null;
editingName: string;
sessions: SessionWithProvider[];
initialSessionsLoaded: boolean;
isLoadingSessions: boolean;
currentTime: Date;
editingSession: string | null;
editingSessionName: string;
tasksEnabled: boolean;
mcpServerStatus: MCPServerStatus;
onEditingNameChange: (name: string) => void;
onToggleProject: (projectName: string) => void;
onProjectSelect: (project: Project) => void;
onToggleStarProject: (projectName: string) => void;
onStartEditingProject: (project: Project) => void;
onCancelEditingProject: () => void;
onSaveProjectName: (projectName: string) => void;
onDeleteProject: (project: Project) => void;
onSessionSelect: (session: SessionWithProvider, projectName: string) => void;
onDeleteSession: (
projectName: string,
sessionId: string,
sessionTitle: string,
provider: SessionProvider,
) => void;
onLoadMoreSessions: (project: Project) => void;
onNewSession: (project: Project) => void;
onEditingSessionNameChange: (value: string) => void;
onStartEditingSession: (sessionId: string, initialName: string) => void;
onCancelEditingSession: () => void;
onSaveEditingSession: (projectName: string, sessionId: string, summary: string) => void;
touchHandlerFactory: TouchHandlerFactory;
t: TFunction;
};
const getSessionCountDisplay = (sessions: SessionWithProvider[], hasMoreSessions: boolean): string => {
const sessionCount = sessions.length;
if (hasMoreSessions && sessionCount >= 5) {
return `${sessionCount}+`;
}
return `${sessionCount}`;
};
export default function SidebarProjectItem({
project,
selectedProject,
selectedSession,
isExpanded,
isDeleting,
isStarred,
editingProject,
editingName,
sessions,
initialSessionsLoaded,
isLoadingSessions,
currentTime,
editingSession,
editingSessionName,
tasksEnabled,
mcpServerStatus,
onEditingNameChange,
onToggleProject,
onProjectSelect,
onToggleStarProject,
onStartEditingProject,
onCancelEditingProject,
onSaveProjectName,
onDeleteProject,
onSessionSelect,
onDeleteSession,
onLoadMoreSessions,
onNewSession,
onEditingSessionNameChange,
onStartEditingSession,
onCancelEditingSession,
onSaveEditingSession,
touchHandlerFactory,
t,
}: SidebarProjectItemProps) {
const isSelected = selectedProject?.name === project.name;
const isEditing = editingProject === project.name;
const hasMoreSessions = project.sessionMeta?.hasMore === true;
const sessionCountDisplay = getSessionCountDisplay(sessions, hasMoreSessions);
const sessionCountLabel = `${sessionCountDisplay} session${sessions.length === 1 ? '' : 's'}`;
const taskStatus = getTaskIndicatorStatus(project, mcpServerStatus);
const toggleProject = () => onToggleProject(project.name);
const toggleStarProject = () => onToggleStarProject(project.name);
const saveProjectName = () => {
onSaveProjectName(project.name);
};
const selectAndToggleProject = () => {
if (selectedProject?.name !== project.name) {
onProjectSelect(project);
}
toggleProject();
};
return (
<div className={cn('md:space-y-1', isDeleting && 'opacity-50 pointer-events-none')}>
<div className="group md:group">
<div className="md:hidden">
<div
className={cn(
'p-3 mx-3 my-1 rounded-lg bg-card border border-border/50 active:scale-[0.98] transition-all duration-150',
isSelected && 'bg-primary/5 border-primary/20',
isStarred &&
!isSelected &&
'bg-yellow-50/50 dark:bg-yellow-900/5 border-yellow-200/30 dark:border-yellow-800/30',
)}
onClick={toggleProject}
onTouchEnd={touchHandlerFactory(toggleProject)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 min-w-0 flex-1">
<div
className={cn(
'w-8 h-8 rounded-lg flex items-center justify-center transition-colors',
isExpanded ? 'bg-primary/10' : 'bg-muted',
)}
>
{isExpanded ? (
<FolderOpen className="w-4 h-4 text-primary" />
) : (
<Folder className="w-4 h-4 text-muted-foreground" />
)}
</div>
<div className="min-w-0 flex-1">
{isEditing ? (
<input
type="text"
value={editingName}
onChange={(event) => onEditingNameChange(event.target.value)}
className="w-full px-3 py-2 text-sm border-2 border-primary/40 focus:border-primary rounded-lg bg-background text-foreground shadow-sm focus:shadow-md transition-all duration-200 focus:outline-none"
placeholder={t('projects.projectNamePlaceholder')}
autoFocus
autoComplete="off"
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => {
if (event.key === 'Enter') {
saveProjectName();
}
if (event.key === 'Escape') {
onCancelEditingProject();
}
}}
style={{
fontSize: '16px',
WebkitAppearance: 'none',
borderRadius: '8px',
}}
/>
) : (
<>
<div className="flex items-center justify-between min-w-0 flex-1">
<h3 className="text-sm font-medium text-foreground truncate">{project.displayName}</h3>
{tasksEnabled && (
<TaskIndicator
status={taskStatus}
size="xs"
className="hidden md:inline-flex flex-shrink-0 ml-2"
/>
)}
</div>
<p className="text-xs text-muted-foreground">{sessionCountLabel}</p>
</>
)}
</div>
</div>
<div className="flex items-center gap-1">
{isEditing ? (
<>
<button
className="w-8 h-8 rounded-lg bg-green-500 dark:bg-green-600 flex items-center justify-center active:scale-90 transition-all duration-150 shadow-sm active:shadow-none"
onClick={(event) => {
event.stopPropagation();
saveProjectName();
}}
>
<Check className="w-4 h-4 text-white" />
</button>
<button
className="w-8 h-8 rounded-lg bg-gray-500 dark:bg-gray-600 flex items-center justify-center active:scale-90 transition-all duration-150 shadow-sm active:shadow-none"
onClick={(event) => {
event.stopPropagation();
onCancelEditingProject();
}}
>
<X className="w-4 h-4 text-white" />
</button>
</>
) : (
<>
<button
className={cn(
'w-8 h-8 rounded-lg flex items-center justify-center active:scale-90 transition-all duration-150 border',
isStarred
? 'bg-yellow-500/10 dark:bg-yellow-900/30 border-yellow-200 dark:border-yellow-800'
: 'bg-gray-500/10 dark:bg-gray-900/30 border-gray-200 dark:border-gray-800',
)}
onClick={(event) => {
event.stopPropagation();
toggleStarProject();
}}
onTouchEnd={touchHandlerFactory(toggleStarProject)}
title={isStarred ? t('tooltips.removeFromFavorites') : t('tooltips.addToFavorites')}
>
<Star
className={cn(
'w-4 h-4 transition-colors',
isStarred
? 'text-yellow-600 dark:text-yellow-400 fill-current'
: 'text-gray-600 dark:text-gray-400',
)}
/>
</button>
<button
className="w-8 h-8 rounded-lg bg-red-500/10 dark:bg-red-900/30 flex items-center justify-center active:scale-90 border border-red-200 dark:border-red-800"
onClick={(event) => {
event.stopPropagation();
onDeleteProject(project);
}}
onTouchEnd={touchHandlerFactory(() => onDeleteProject(project))}
>
<Trash2 className="w-4 h-4 text-red-600 dark:text-red-400" />
</button>
<button
className="w-8 h-8 rounded-lg bg-primary/10 dark:bg-primary/20 flex items-center justify-center active:scale-90 border border-primary/20 dark:border-primary/30"
onClick={(event) => {
event.stopPropagation();
onStartEditingProject(project);
}}
onTouchEnd={touchHandlerFactory(() => onStartEditingProject(project))}
>
<Edit3 className="w-4 h-4 text-primary" />
</button>
<div className="w-6 h-6 rounded-md bg-muted/30 flex items-center justify-center">
{isExpanded ? (
<ChevronDown className="w-3 h-3 text-muted-foreground" />
) : (
<ChevronRight className="w-3 h-3 text-muted-foreground" />
)}
</div>
</>
)}
</div>
</div>
</div>
</div>
<Button
variant="ghost"
className={cn(
'hidden md:flex w-full justify-between p-2 h-auto font-normal hover:bg-accent/50',
isSelected && 'bg-accent text-accent-foreground',
isStarred &&
!isSelected &&
'bg-yellow-50/50 dark:bg-yellow-900/10 hover:bg-yellow-100/50 dark:hover:bg-yellow-900/20',
)}
onClick={selectAndToggleProject}
onTouchEnd={touchHandlerFactory(selectAndToggleProject)}
>
<div className="flex items-center gap-3 min-w-0 flex-1">
{isExpanded ? (
<FolderOpen className="w-4 h-4 text-primary flex-shrink-0" />
) : (
<Folder className="w-4 h-4 text-muted-foreground flex-shrink-0" />
)}
<div className="min-w-0 flex-1 text-left">
{isEditing ? (
<div className="space-y-1">
<input
type="text"
value={editingName}
onChange={(event) => onEditingNameChange(event.target.value)}
className="w-full px-2 py-1 text-sm border border-border rounded bg-background text-foreground focus:ring-2 focus:ring-primary/20"
placeholder={t('projects.projectNamePlaceholder')}
autoFocus
onKeyDown={(event) => {
if (event.key === 'Enter') {
saveProjectName();
}
if (event.key === 'Escape') {
onCancelEditingProject();
}
}}
/>
<div className="text-xs text-muted-foreground truncate" title={project.fullPath}>
{project.fullPath}
</div>
</div>
) : (
<div>
<div className="text-sm font-semibold truncate text-foreground" title={project.displayName}>
{project.displayName}
</div>
<div className="text-xs text-muted-foreground">
{sessionCountDisplay}
{project.fullPath !== project.displayName && (
<span className="ml-1 opacity-60" title={project.fullPath}>
{' - '}
{project.fullPath.length > 25 ? `...${project.fullPath.slice(-22)}` : project.fullPath}
</span>
)}
</div>
</div>
)}
</div>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
{isEditing ? (
<>
<div
className="w-6 h-6 text-green-600 hover:text-green-700 hover:bg-green-50 dark:hover:bg-green-900/20 flex items-center justify-center rounded cursor-pointer transition-colors"
onClick={(event) => {
event.stopPropagation();
saveProjectName();
}}
>
<Check className="w-3 h-3" />
</div>
<div
className="w-6 h-6 text-gray-500 hover:text-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 flex items-center justify-center rounded cursor-pointer transition-colors"
onClick={(event) => {
event.stopPropagation();
onCancelEditingProject();
}}
>
<X className="w-3 h-3" />
</div>
</>
) : (
<>
<div
className={cn(
'w-6 h-6 opacity-0 group-hover:opacity-100 transition-all duration-200 flex items-center justify-center rounded cursor-pointer touch:opacity-100',
isStarred ? 'hover:bg-yellow-50 dark:hover:bg-yellow-900/20 opacity-100' : 'hover:bg-accent',
)}
onClick={(event) => {
event.stopPropagation();
toggleStarProject();
}}
title={isStarred ? t('tooltips.removeFromFavorites') : t('tooltips.addToFavorites')}
>
<Star
className={cn(
'w-3 h-3 transition-colors',
isStarred
? 'text-yellow-600 dark:text-yellow-400 fill-current'
: 'text-muted-foreground',
)}
/>
</div>
<div
className="w-6 h-6 opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-accent flex items-center justify-center rounded cursor-pointer touch:opacity-100"
onClick={(event) => {
event.stopPropagation();
onStartEditingProject(project);
}}
title={t('tooltips.renameProject')}
>
<Edit3 className="w-3 h-3" />
</div>
<div
className="w-6 h-6 opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center justify-center rounded cursor-pointer touch:opacity-100"
onClick={(event) => {
event.stopPropagation();
onDeleteProject(project);
}}
title={t('tooltips.deleteProject')}
>
<Trash2 className="w-3 h-3 text-red-600 dark:text-red-400" />
</div>
{isExpanded ? (
<ChevronDown className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors" />
) : (
<ChevronRight className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors" />
)}
</>
)}
</div>
</Button>
</div>
<SidebarProjectSessions
project={project}
isExpanded={isExpanded}
sessions={sessions}
selectedSession={selectedSession}
initialSessionsLoaded={initialSessionsLoaded}
isLoadingSessions={isLoadingSessions}
currentTime={currentTime}
editingSession={editingSession}
editingSessionName={editingSessionName}
onEditingSessionNameChange={onEditingSessionNameChange}
onStartEditingSession={onStartEditingSession}
onCancelEditingSession={onCancelEditingSession}
onSaveEditingSession={onSaveEditingSession}
onProjectSelect={onProjectSelect}
onSessionSelect={onSessionSelect}
onDeleteSession={onDeleteSession}
onLoadMoreSessions={onLoadMoreSessions}
onNewSession={onNewSession}
touchHandlerFactory={touchHandlerFactory}
t={t}
/>
</div>
);
}

View File

@@ -0,0 +1,153 @@
import type { TFunction } from 'i18next';
import type { LoadingProgress, Project, ProjectSession, SessionProvider } from '../../../../types/app';
import type {
LoadingSessionsByProject,
MCPServerStatus,
SessionWithProvider,
TouchHandlerFactory,
} from '../../types/types';
import SidebarProjectItem from './SidebarProjectItem';
import SidebarProjectsState from './SidebarProjectsState';
export type SidebarProjectListProps = {
projects: Project[];
filteredProjects: Project[];
selectedProject: Project | null;
selectedSession: ProjectSession | null;
isLoading: boolean;
loadingProgress: LoadingProgress | null;
expandedProjects: Set<string>;
editingProject: string | null;
editingName: string;
loadingSessions: LoadingSessionsByProject;
initialSessionsLoaded: Set<string>;
currentTime: Date;
editingSession: string | null;
editingSessionName: string;
deletingProjects: Set<string>;
tasksEnabled: boolean;
mcpServerStatus: MCPServerStatus;
getProjectSessions: (project: Project) => SessionWithProvider[];
isProjectStarred: (projectName: string) => boolean;
onEditingNameChange: (value: string) => void;
onToggleProject: (projectName: string) => void;
onProjectSelect: (project: Project) => void;
onToggleStarProject: (projectName: string) => void;
onStartEditingProject: (project: Project) => void;
onCancelEditingProject: () => void;
onSaveProjectName: (projectName: string) => void;
onDeleteProject: (project: Project) => void;
onSessionSelect: (session: SessionWithProvider, projectName: string) => void;
onDeleteSession: (
projectName: string,
sessionId: string,
sessionTitle: string,
provider: SessionProvider,
) => void;
onLoadMoreSessions: (project: Project) => void;
onNewSession: (project: Project) => void;
onEditingSessionNameChange: (value: string) => void;
onStartEditingSession: (sessionId: string, initialName: string) => void;
onCancelEditingSession: () => void;
onSaveEditingSession: (projectName: string, sessionId: string, summary: string) => void;
touchHandlerFactory: TouchHandlerFactory;
t: TFunction;
};
export default function SidebarProjectList({
projects,
filteredProjects,
selectedProject,
selectedSession,
isLoading,
loadingProgress,
expandedProjects,
editingProject,
editingName,
loadingSessions,
initialSessionsLoaded,
currentTime,
editingSession,
editingSessionName,
deletingProjects,
tasksEnabled,
mcpServerStatus,
getProjectSessions,
isProjectStarred,
onEditingNameChange,
onToggleProject,
onProjectSelect,
onToggleStarProject,
onStartEditingProject,
onCancelEditingProject,
onSaveProjectName,
onDeleteProject,
onSessionSelect,
onDeleteSession,
onLoadMoreSessions,
onNewSession,
onEditingSessionNameChange,
onStartEditingSession,
onCancelEditingSession,
onSaveEditingSession,
touchHandlerFactory,
t,
}: SidebarProjectListProps) {
const state = (
<SidebarProjectsState
isLoading={isLoading}
loadingProgress={loadingProgress}
projectsCount={projects.length}
filteredProjectsCount={filteredProjects.length}
t={t}
/>
);
const showProjects = !isLoading && projects.length > 0 && filteredProjects.length > 0;
return (
<div className="md:space-y-1 pb-safe-area-inset-bottom">
{!showProjects
? state
: filteredProjects.map((project) => (
<SidebarProjectItem
key={project.name}
project={project}
selectedProject={selectedProject}
selectedSession={selectedSession}
isExpanded={expandedProjects.has(project.name)}
isDeleting={deletingProjects.has(project.name)}
isStarred={isProjectStarred(project.name)}
editingProject={editingProject}
editingName={editingName}
sessions={getProjectSessions(project)}
initialSessionsLoaded={initialSessionsLoaded.has(project.name)}
isLoadingSessions={Boolean(loadingSessions[project.name])}
currentTime={currentTime}
editingSession={editingSession}
editingSessionName={editingSessionName}
tasksEnabled={tasksEnabled}
mcpServerStatus={mcpServerStatus}
onEditingNameChange={onEditingNameChange}
onToggleProject={onToggleProject}
onProjectSelect={onProjectSelect}
onToggleStarProject={onToggleStarProject}
onStartEditingProject={onStartEditingProject}
onCancelEditingProject={onCancelEditingProject}
onSaveProjectName={onSaveProjectName}
onDeleteProject={onDeleteProject}
onSessionSelect={onSessionSelect}
onDeleteSession={onDeleteSession}
onLoadMoreSessions={onLoadMoreSessions}
onNewSession={onNewSession}
onEditingSessionNameChange={onEditingSessionNameChange}
onStartEditingSession={onStartEditingSession}
onCancelEditingSession={onCancelEditingSession}
onSaveEditingSession={onSaveEditingSession}
touchHandlerFactory={touchHandlerFactory}
t={t}
/>
))}
</div>
);
}

View File

@@ -0,0 +1,160 @@
import { ChevronDown, Plus } from 'lucide-react';
import type { TFunction } from 'i18next';
import { Button } from '../../../ui/button';
import type { Project, ProjectSession, SessionProvider } from '../../../../types/app';
import type { SessionWithProvider, TouchHandlerFactory } from '../../types/types';
import SidebarSessionItem from './SidebarSessionItem';
type SidebarProjectSessionsProps = {
project: Project;
isExpanded: boolean;
sessions: SessionWithProvider[];
selectedSession: ProjectSession | null;
initialSessionsLoaded: boolean;
isLoadingSessions: boolean;
currentTime: Date;
editingSession: string | null;
editingSessionName: string;
onEditingSessionNameChange: (value: string) => void;
onStartEditingSession: (sessionId: string, initialName: string) => void;
onCancelEditingSession: () => void;
onSaveEditingSession: (projectName: string, sessionId: string, summary: string) => void;
onProjectSelect: (project: Project) => void;
onSessionSelect: (session: SessionWithProvider, projectName: string) => void;
onDeleteSession: (
projectName: string,
sessionId: string,
sessionTitle: string,
provider: SessionProvider,
) => void;
onLoadMoreSessions: (project: Project) => void;
onNewSession: (project: Project) => void;
touchHandlerFactory: TouchHandlerFactory;
t: TFunction;
};
function SessionListSkeleton() {
return (
<>
{Array.from({ length: 3 }).map((_, index) => (
<div key={index} className="p-2 rounded-md">
<div className="flex items-start gap-2">
<div className="w-3 h-3 bg-muted rounded-full animate-pulse mt-0.5" />
<div className="flex-1 space-y-1">
<div className="h-3 bg-muted rounded animate-pulse" style={{ width: `${60 + index * 15}%` }} />
<div className="h-2 bg-muted rounded animate-pulse w-1/2" />
</div>
</div>
</div>
))}
</>
);
}
export default function SidebarProjectSessions({
project,
isExpanded,
sessions,
selectedSession,
initialSessionsLoaded,
isLoadingSessions,
currentTime,
editingSession,
editingSessionName,
onEditingSessionNameChange,
onStartEditingSession,
onCancelEditingSession,
onSaveEditingSession,
onProjectSelect,
onSessionSelect,
onDeleteSession,
onLoadMoreSessions,
onNewSession,
touchHandlerFactory,
t,
}: SidebarProjectSessionsProps) {
if (!isExpanded) {
return null;
}
const hasSessions = sessions.length > 0;
const hasMoreSessions = project.sessionMeta?.hasMore === true;
return (
<div className="ml-3 space-y-1 border-l border-border pl-3">
{!initialSessionsLoaded ? (
<SessionListSkeleton />
) : !hasSessions && !isLoadingSessions ? (
<div className="py-2 px-3 text-left">
<p className="text-xs text-muted-foreground">{t('sessions.noSessions')}</p>
</div>
) : (
sessions.map((session) => (
<SidebarSessionItem
key={session.id}
project={project}
session={session}
selectedSession={selectedSession}
currentTime={currentTime}
editingSession={editingSession}
editingSessionName={editingSessionName}
onEditingSessionNameChange={onEditingSessionNameChange}
onStartEditingSession={onStartEditingSession}
onCancelEditingSession={onCancelEditingSession}
onSaveEditingSession={onSaveEditingSession}
onProjectSelect={onProjectSelect}
onSessionSelect={onSessionSelect}
onDeleteSession={onDeleteSession}
touchHandlerFactory={touchHandlerFactory}
t={t}
/>
))
)}
{hasSessions && hasMoreSessions && (
<Button
variant="ghost"
size="sm"
className="w-full justify-center gap-2 mt-2 text-muted-foreground"
onClick={() => onLoadMoreSessions(project)}
disabled={isLoadingSessions}
>
{isLoadingSessions ? (
<>
<div className="w-3 h-3 animate-spin rounded-full border border-muted-foreground border-t-transparent" />
{t('sessions.loading')}
</>
) : (
<>
<ChevronDown className="w-3 h-3" />
{t('sessions.showMore')}
</>
)}
</Button>
)}
<div className="md:hidden px-3 pb-2">
<button
className="w-full h-8 bg-primary hover:bg-primary/90 text-primary-foreground rounded-md flex items-center justify-center gap-2 font-medium text-xs active:scale-[0.98] transition-all duration-150"
onClick={() => {
onProjectSelect(project);
onNewSession(project);
}}
>
<Plus className="w-3 h-3" />
{t('sessions.newSession')}
</button>
</div>
<Button
variant="default"
size="sm"
className="hidden md:flex w-full justify-start gap-2 mt-1 h-8 text-xs font-medium bg-primary hover:bg-primary/90 text-primary-foreground transition-colors"
onClick={() => onNewSession(project)}
>
<Plus className="w-3 h-3" />
{t('sessions.newSession')}
</Button>
</div>
);
}

View File

@@ -0,0 +1,79 @@
import { Folder, Search } from 'lucide-react';
import type { TFunction } from 'i18next';
import type { LoadingProgress } from '../../../../types/app';
type SidebarProjectsStateProps = {
isLoading: boolean;
loadingProgress: LoadingProgress | null;
projectsCount: number;
filteredProjectsCount: number;
t: TFunction;
};
export default function SidebarProjectsState({
isLoading,
loadingProgress,
projectsCount,
filteredProjectsCount,
t,
}: SidebarProjectsStateProps) {
if (isLoading) {
return (
<div className="text-center py-12 md:py-8 px-4">
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-4 md:mb-3">
<div className="w-6 h-6 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
</div>
<h3 className="text-base font-medium text-foreground mb-2 md:mb-1">{t('projects.loadingProjects')}</h3>
{loadingProgress && loadingProgress.total > 0 ? (
<div className="space-y-2">
<div className="w-full bg-muted rounded-full h-2 overflow-hidden">
<div
className="bg-primary h-full transition-all duration-300 ease-out"
style={{ width: `${(loadingProgress.current / loadingProgress.total) * 100}%` }}
/>
</div>
<p className="text-sm text-muted-foreground">
{loadingProgress.current}/{loadingProgress.total} {t('projects.projects')}
</p>
{loadingProgress.currentProject && (
<p
className="text-xs text-muted-foreground/70 truncate max-w-[200px] mx-auto"
title={loadingProgress.currentProject}
>
{loadingProgress.currentProject.split('-').slice(-2).join('/')}
</p>
)}
</div>
) : (
<p className="text-sm text-muted-foreground">{t('projects.fetchingProjects')}</p>
)}
</div>
);
}
if (projectsCount === 0) {
return (
<div className="text-center py-12 md:py-8 px-4">
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-4 md:mb-3">
<Folder className="w-6 h-6 text-muted-foreground" />
</div>
<h3 className="text-base font-medium text-foreground mb-2 md:mb-1">{t('projects.noProjects')}</h3>
<p className="text-sm text-muted-foreground">{t('projects.runClaudeCli')}</p>
</div>
);
}
if (filteredProjectsCount === 0) {
return (
<div className="text-center py-12 md:py-8 px-4">
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-4 md:mb-3">
<Search className="w-6 h-6 text-muted-foreground" />
</div>
<h3 className="text-base font-medium text-foreground mb-2 md:mb-1">{t('projects.noMatchingProjects')}</h3>
<p className="text-sm text-muted-foreground">{t('projects.tryDifferentSearch')}</p>
</div>
);
}
return null;
}

View File

@@ -0,0 +1,239 @@
import { Badge } from '../../../ui/badge';
import { Button } from '../../../ui/button';
import { Check, Clock, Edit2, Trash2, X } from 'lucide-react';
import type { TFunction } from 'i18next';
import { cn } from '../../../../lib/utils';
import { formatTimeAgo } from '../../../../utils/dateUtils';
import type { Project, ProjectSession, SessionProvider } from '../../../../types/app';
import type { SessionWithProvider, TouchHandlerFactory } from '../../types/types';
import { createSessionViewModel } from '../../utils/utils';
import SessionProviderLogo from '../../../SessionProviderLogo';
type SidebarSessionItemProps = {
project: Project;
session: SessionWithProvider;
selectedSession: ProjectSession | null;
currentTime: Date;
editingSession: string | null;
editingSessionName: string;
onEditingSessionNameChange: (value: string) => void;
onStartEditingSession: (sessionId: string, initialName: string) => void;
onCancelEditingSession: () => void;
onSaveEditingSession: (projectName: string, sessionId: string, summary: string) => void;
onProjectSelect: (project: Project) => void;
onSessionSelect: (session: SessionWithProvider, projectName: string) => void;
onDeleteSession: (
projectName: string,
sessionId: string,
sessionTitle: string,
provider: SessionProvider,
) => void;
touchHandlerFactory: TouchHandlerFactory;
t: TFunction;
};
export default function SidebarSessionItem({
project,
session,
selectedSession,
currentTime,
editingSession,
editingSessionName,
onEditingSessionNameChange,
onStartEditingSession,
onCancelEditingSession,
onSaveEditingSession,
onProjectSelect,
onSessionSelect,
onDeleteSession,
touchHandlerFactory,
t,
}: SidebarSessionItemProps) {
const sessionView = createSessionViewModel(session, currentTime, t);
const isSelected = selectedSession?.id === session.id;
const selectMobileSession = () => {
onProjectSelect(project);
onSessionSelect(session, project.name);
};
const saveEditedSession = () => {
onSaveEditingSession(project.name, session.id, editingSessionName);
};
const requestDeleteSession = () => {
onDeleteSession(project.name, session.id, sessionView.sessionName, session.__provider);
};
return (
<div className="group relative">
{sessionView.isActive && (
<div className="absolute left-0 top-1/2 transform -translate-y-1/2 -translate-x-1">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
</div>
)}
<div className="md:hidden">
<div
className={cn(
'p-2 mx-3 my-0.5 rounded-md bg-card border active:scale-[0.98] transition-all duration-150 relative',
isSelected ? 'bg-primary/5 border-primary/20' : '',
!isSelected && sessionView.isActive
? 'border-green-500/30 bg-green-50/5 dark:bg-green-900/5'
: 'border-border/30',
)}
onClick={selectMobileSession}
onTouchEnd={touchHandlerFactory(selectMobileSession)}
>
<div className="flex items-center gap-2">
<div
className={cn(
'w-5 h-5 rounded-md flex items-center justify-center flex-shrink-0',
isSelected ? 'bg-primary/10' : 'bg-muted/50',
)}
>
<SessionProviderLogo provider={session.__provider} className="w-3 h-3" />
</div>
<div className="min-w-0 flex-1">
<div className="text-xs font-medium truncate text-foreground">{sessionView.sessionName}</div>
<div className="flex items-center gap-1 mt-0.5">
<Clock className="w-2.5 h-2.5 text-muted-foreground" />
<span className="text-xs text-muted-foreground">
{formatTimeAgo(sessionView.sessionTime, currentTime, t)}
</span>
{sessionView.messageCount > 0 && (
<Badge variant="secondary" className="text-xs px-1 py-0 ml-auto">
{sessionView.messageCount}
</Badge>
)}
<span className="ml-1 opacity-70">
<SessionProviderLogo provider={session.__provider} className="w-3 h-3" />
</span>
</div>
</div>
{!sessionView.isCursorSession && (
<button
className="w-5 h-5 rounded-md bg-red-50 dark:bg-red-900/20 flex items-center justify-center active:scale-95 transition-transform opacity-70 ml-1"
onClick={(event) => {
event.stopPropagation();
requestDeleteSession();
}}
onTouchEnd={touchHandlerFactory(requestDeleteSession)}
>
<Trash2 className="w-2.5 h-2.5 text-red-600 dark:text-red-400" />
</button>
)}
</div>
</div>
</div>
<div className="hidden md:block">
<Button
variant="ghost"
className={cn(
'w-full justify-start p-2 h-auto font-normal text-left hover:bg-accent/50 transition-colors duration-200',
isSelected && 'bg-accent text-accent-foreground',
)}
onClick={() => onSessionSelect(session, project.name)}
onTouchEnd={touchHandlerFactory(() => onSessionSelect(session, project.name))}
>
<div className="flex items-start gap-2 min-w-0 w-full">
<SessionProviderLogo provider={session.__provider} className="w-3 h-3 mt-0.5 flex-shrink-0" />
<div className="min-w-0 flex-1">
<div className="text-xs font-medium truncate text-foreground">{sessionView.sessionName}</div>
<div className="flex items-center gap-1 mt-0.5">
<Clock className="w-2.5 h-2.5 text-muted-foreground" />
<span className="text-xs text-muted-foreground">
{formatTimeAgo(sessionView.sessionTime, currentTime, t)}
</span>
{sessionView.messageCount > 0 && (
<Badge
variant="secondary"
className="text-xs px-1 py-0 ml-auto group-hover:opacity-0 transition-opacity"
>
{sessionView.messageCount}
</Badge>
)}
<span className="ml-1 opacity-70 group-hover:opacity-0 transition-opacity">
<SessionProviderLogo provider={session.__provider} className="w-3 h-3" />
</span>
</div>
</div>
</div>
</Button>
{!sessionView.isCursorSession && (
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-all duration-200">
{editingSession === session.id && !sessionView.isCodexSession ? (
<>
<input
type="text"
value={editingSessionName}
onChange={(event) => onEditingSessionNameChange(event.target.value)}
onKeyDown={(event) => {
event.stopPropagation();
if (event.key === 'Enter') {
saveEditedSession();
} else if (event.key === 'Escape') {
onCancelEditingSession();
}
}}
onClick={(event) => event.stopPropagation()}
className="w-32 px-2 py-1 text-xs border border-border rounded bg-background focus:outline-none focus:ring-1 focus:ring-primary"
autoFocus
/>
<button
className="w-6 h-6 bg-green-50 hover:bg-green-100 dark:bg-green-900/20 dark:hover:bg-green-900/40 rounded flex items-center justify-center"
onClick={(event) => {
event.stopPropagation();
saveEditedSession();
}}
title={t('tooltips.save')}
>
<Check className="w-3 h-3 text-green-600 dark:text-green-400" />
</button>
<button
className="w-6 h-6 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900/20 dark:hover:bg-gray-900/40 rounded flex items-center justify-center"
onClick={(event) => {
event.stopPropagation();
onCancelEditingSession();
}}
title={t('tooltips.cancel')}
>
<X className="w-3 h-3 text-gray-600 dark:text-gray-400" />
</button>
</>
) : (
<>
{!sessionView.isCodexSession && (
<button
className="w-6 h-6 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900/20 dark:hover:bg-gray-900/40 rounded flex items-center justify-center"
onClick={(event) => {
event.stopPropagation();
onStartEditingSession(session.id, session.summary || t('projects.newSession'));
}}
title={t('tooltips.editSessionName')}
>
<Edit2 className="w-3 h-3 text-gray-600 dark:text-gray-400" />
</button>
)}
<button
className="w-6 h-6 bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/40 rounded flex items-center justify-center"
onClick={(event) => {
event.stopPropagation();
requestDeleteSession();
}}
title={t('tooltips.deleteSession')}
>
<Trash2 className="w-3 h-3 text-red-600 dark:text-red-400" />
</button>
</>
)}
</div>
)}
</div>
</div>
);
}