From 5c693bf3d86c9cf735c7d49d846fc67c6d47e2eb Mon Sep 17 00:00:00 2001 From: Mathias Beaulieu-Duncan Date: Tue, 7 Apr 2026 13:45:24 -0400 Subject: [PATCH] feat: Browse picks folder, Load picks single file, recursive .jsonl scan - Browse button now opens a folder picker and recursively scans all subfolders for .jsonl files, displaying them in the session list - New Load button for loading a single .jsonl file (old Browse behavior) - Default path remains ~/.claude/projects on launch - Signed, notarized, stapled DMG --- lib/screens/home/home_screen.dart | 82 ++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index 8a7e59c..63bf463 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -100,6 +100,7 @@ class _HomeScreenState extends State { bool _scanning = true; String _searchQuery = ''; _Project? _selectedProject; + _Project? _customFolderProject; @override void initState() { @@ -290,7 +291,68 @@ class _HomeScreenState extends State { } } - Future _pickFile() async { + Future _pickFolder() async { + final result = await FilePicker.platform.getDirectoryPath( + dialogTitle: 'Select a folder containing Claude session files', + ); + if (result != null) { + await _scanFolder(result); + } + } + + Future _scanFolder(String folderPath) async { + setState(() { + _scanning = true; + _selectedProject = null; + }); + + final sessions = <_SessionFile>[]; + + // Recursively scan for .jsonl files + final dir = Directory(folderPath); + if (await dir.exists()) { + await _scanFolderRecursive(dir, sessions); + } + + sessions.sort((a, b) => b.stat.modified.compareTo(a.stat.modified)); + + if (mounted) { + setState(() { + _customFolderProject = _Project( + dirPath: folderPath, + rawDirName: folderPath.split('/').last, + displayName: folderPath.split('/').last, + fullPath: folderPath, + sessions: sessions, + lastModified: sessions.isNotEmpty ? sessions.first.stat.modified : DateTime.now(), + ); + _selectedProject = _customFolderProject; + _scanning = false; + }); + } + } + + Future _scanFolderRecursive(Directory dir, List<_SessionFile> sessions) async { + try { + await for (final entity in dir.list()) { + if (entity is File && entity.path.endsWith('.jsonl')) { + try { + final stat = entity.statSync(); + final fileName = entity.path.split('/').last; + sessions.add(_SessionFile( + file: entity, + stat: stat, + sessionId: fileName.replaceAll('.jsonl', ''), + )); + } catch (_) {} + } else if (entity is Directory) { + await _scanFolderRecursive(entity, sessions); + } + } + } catch (_) {} + } + + Future _loadSingleFile() async { final home = _getRealHome() ?? ''; final result = await FilePicker.platform.pickFiles( type: FileType.custom, @@ -438,7 +500,23 @@ class _HomeScreenState extends State { ), const SizedBox(width: 12), ElevatedButton.icon( - onPressed: _pickFile, + onPressed: _loadSingleFile, + icon: const Icon(Icons.insert_drive_file_outlined, size: 14), + label: const Text('Load'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.surfaceLight, + foregroundColor: AppColors.textPrimary, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + side: const BorderSide(color: AppColors.surfaceBorder), + ), + textStyle: const TextStyle(fontSize: 12), + ), + ), + const SizedBox(width: 8), + ElevatedButton.icon( + onPressed: _pickFolder, icon: const Icon(Icons.folder_open, size: 14), label: const Text('Browse'), style: ElevatedButton.styleFrom(