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
This commit is contained in:
Mathias Beaulieu-Duncan 2026-04-07 13:45:24 -04:00
parent e963b2edcd
commit 5c693bf3d8

View File

@ -100,6 +100,7 @@ class _HomeScreenState extends State<HomeScreen> {
bool _scanning = true;
String _searchQuery = '';
_Project? _selectedProject;
_Project? _customFolderProject;
@override
void initState() {
@ -290,7 +291,68 @@ class _HomeScreenState extends State<HomeScreen> {
}
}
Future<void> _pickFile() async {
Future<void> _pickFolder() async {
final result = await FilePicker.platform.getDirectoryPath(
dialogTitle: 'Select a folder containing Claude session files',
);
if (result != null) {
await _scanFolder(result);
}
}
Future<void> _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<void> _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<void> _loadSingleFile() async {
final home = _getRealHome() ?? '';
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
@ -438,7 +500,23 @@ class _HomeScreenState extends State<HomeScreen> {
),
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(