import "package:provider/provider.dart"; import "package:shadcn_flutter/shadcn_flutter.dart"; import '../../../src/session/session_types.dart'; import '../../providers/chat_provider.dart'; import '../../providers/home_coordinator.dart'; import '../../providers/projects_provider.dart'; import '../../providers/session_provider.dart'; import 'app_logo.dart'; import 'project_section.dart'; import 'thread_button.dart'; class Sidebar extends StatelessWidget { const Sidebar({super.key}); @override Widget build(BuildContext context) { final projectsProvider = context.watch(); final sessionProvider = context.watch(); final chatProvider = context.watch(); final coordinator = context.read(); return Container( width: 300, color: Theme.of(context).colorScheme.input.scaleAlpha(0.3), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( height: 100, alignment: Alignment.centerLeft, padding: EdgeInsets.symmetric(horizontal: 16), child: AppLogo(), ), Divider(), Gap(16), Padding( padding: EdgeInsets.symmetric(horizontal: 8), child: _fixedSection(context, coordinator, chatProvider), ), Gap(16), Divider(), Gap(16), Expanded( child: Padding( padding: EdgeInsets.symmetric(horizontal: 8), child: _projectsSection(context, projectsProvider, sessionProvider, coordinator, chatProvider), ), ), ], ), ); } Widget _fixedSection(BuildContext context, HomeCoordinator coordinator, ChatProvider chatProvider) { return Column( children: [ SizedBox( width: double.infinity, child: Button.ghost( style: ButtonStyle.ghost().copyWith( padding: (context, state, edgeInsets) { return EdgeInsets.only( top: 8, left: 8, bottom: 8, right: 10 ); } ), onPressed: chatProvider.isLoading ? null : coordinator.createNewChat, disableFocusOutline: true, leading: Icon(LucideIcons.squarePen).iconSmall, alignment: Alignment.centerLeft, child: Text("New Chat"), ), ), SizedBox( width: double.infinity, child: Button.ghost( style: ButtonStyle.ghost().copyWith( padding: (context, state, edgeInsets) { return EdgeInsets.only( top: 8, left: 8, bottom: 8, right: 10 ); } ), onPressed: coordinator.pickProjectDirectory, disableFocusOutline: true, leading: Icon(LucideIcons.folderPlus).iconSmall, alignment: Alignment.centerLeft, child: Text("New Project"), ), ), ], ); } Widget _projectsSection(BuildContext context, ProjectsProvider projectsProvider, SessionProvider sessionProvider, HomeCoordinator coordinator, ChatProvider chatProvider) { if (projectsProvider.projects.isEmpty) { return Padding( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: Text("No projects yet").textSmall.muted, ); } // group sessions by working directory final sessionsByProject = >{}; for (final session in sessionProvider.sessions) { final dir = session.workingDirectory ?? ''; sessionsByProject.putIfAbsent(dir, () => []).add(session); } // sort sessions within each project newest first final sorted = >{}; sessionsByProject.forEach((dir, sessions) { sorted[dir] = List.from(sessions) ..sort((a, b) => b.updated.compareTo(a.updated)); }); final projects = List.of(projectsProvider.projects) ..sort((a, b) { final aLatest = sorted[a.workingDirectory]?.firstOrNull?.updated; final bLatest = sorted[b.workingDirectory]?.firstOrNull?.updated; if (aLatest == null && bLatest == null) return 0; if (aLatest == null) return 1; if (bLatest == null) return -1; return bLatest.compareTo(aLatest); }); return ListView( padding: EdgeInsets.zero, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Text("Projects").textMuted, ), Gap(8), for (final project in projects) ...[ ProjectSection( projectLabel: project.name, children: [ if (sorted[project.workingDirectory]?.isEmpty ?? true) ThreadButton( label: "No threads yet", muted: true, ) else for (final session in sorted[project.workingDirectory]!) ThreadButton( label: session.name, lastMessage: session.updated, selected: sessionProvider.currentSessionId == session.id, isRunning: chatProvider.isSessionRunning(session.id), onPressed: () => coordinator.openSession(session), onDelete: () => coordinator.deleteSession(session), ), ], ), Gap(2), ], ], ); } }