import "package:shadcn_flutter/shadcn_flutter.dart"; import "../../../../src/project_store.dart"; import "../../../../src/session/session_types.dart"; import "../../../providers/projects_provider.dart"; import "../../../providers/session_provider.dart"; class ThreadsSection extends StatelessWidget { const ThreadsSection({ required this.projectsProvider, required this.sessionProvider, required this.sessionsByProject, required this.onOpenSession, required this.onSelectProject, required this.onDeleteSession, }); final ProjectsProvider projectsProvider; final SessionProvider sessionProvider; final Map> sessionsByProject; final ValueChanged onOpenSession; final ValueChanged onSelectProject; final ValueChanged onDeleteSession; @override Widget build(BuildContext context) { // Sort sessions by update time (newest first) within each project final sortedSessionsByProject = >{}; sessionsByProject.forEach((workingDirectory, sessions) { final sortedSessions = List.from(sessions) ..sort((a, b) => b.updated.compareTo(a.updated)); sortedSessionsByProject[workingDirectory] = sortedSessions; }); return ListView( padding: const EdgeInsets.fromLTRB(8, 0, 8, 12), children: [ if (projectsProvider.projects.isEmpty) const _SidebarHint(text: "No projects yet") else for (final project in projectsProvider.projects) ...[ // Project header SizedBox( width: double.infinity, child: Button.ghost( onPressed: () => onSelectProject(project), child: Container( padding: const EdgeInsets.symmetric(vertical: 6), child: Text( project.name, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 12, color: Theme.of(context).colorScheme.mutedForeground, ), ), ), ), ), // Project sessions if (sortedSessionsByProject[project.workingDirectory]?.isEmpty ?? true) const Padding( padding: EdgeInsets.fromLTRB(8, 4, 8, 8), child: _SidebarHint(text: "No threads yet"), ) else for (final session in sortedSessionsByProject[project.workingDirectory]!) _SidebarSessionTile( session: session, isSelected: sessionProvider.currentSessionId == session.id, onTap: () => onOpenSession(session), onDelete: () => onDeleteSession(session), ), const Divider(height: 16), ], // Handle sessions that don't belong to any current project if (sortedSessionsByProject.keys.any( (key) => !projectsProvider.projects.any( (project) => project.workingDirectory == key, ), )) ...[ Padding( padding: const EdgeInsets.fromLTRB(8, 8, 8, 4), child: Text( "Sessions Without Projects", style: TextStyle( fontWeight: FontWeight.w600, fontSize: 12, color: Theme.of(context).colorScheme.mutedForeground, ), ), ), for (final entry in sortedSessionsByProject.entries) if (!projectsProvider.projects.any( (project) => project.workingDirectory == entry.key, ) && entry.key.isNotEmpty) for (final session in entry.value) _SidebarSessionTile( session: session, isSelected: sessionProvider.currentSessionId == session.id, onTap: () => onOpenSession(session), onDelete: () => onDeleteSession(session), ), ], ], ); } } class _SidebarHint extends StatelessWidget { const _SidebarHint({required this.text}); final String text; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: Text(text).textSmall.muted, ); } } class _SidebarSessionTile extends StatelessWidget { const _SidebarSessionTile({ required this.session, required this.isSelected, required this.onTap, required this.onDelete, }); final SessionSummary session; final bool isSelected; final VoidCallback onTap; final VoidCallback onDelete; @override Widget build(BuildContext context) { return ContextMenu( items: [ MenuButton( onPressed: (context) { onDelete(); }, child: const Text("Delete"), ), ], child: SizedBox( width: double.infinity, child: Button( style: isSelected ? ButtonStyle.secondary() : ButtonStyle.ghost(), child: Text( session.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontWeight: FontWeight.w400, fontSize: 13), ).textSmall, trailing: Text( _formatRelativeTime(session.updated), style: TextStyle(fontWeight: FontWeight.w400, fontSize: 13), ).muted.textSmall, onPressed: () { onTap(); }, ), ), ); } } String _formatRelativeTime(DateTime timestamp) { final difference = DateTime.now().toUtc().difference(timestamp.toUtc()); if (difference.inMinutes < 1) { return "just now"; } if (difference.inHours < 1) { return "${difference.inMinutes}m"; } if (difference.inDays < 1) { return "${difference.inHours}h"; } if (difference.inDays < 7) { return "${difference.inDays}d"; } final month = timestamp.month.toString().padLeft(2, "0"); final day = timestamp.day.toString().padLeft(2, "0"); return "${timestamp.year}-$month-$day"; }