fix: two-row header layout, flexible path input, button error handling

- Split header into two rows: breadcrumb + count on top, path/buttons below
- Path input is now flexible (fills available width) instead of fixed 320px
- Fixes Row overflow on narrow windows
- Load/Browse buttons now same height (36px) as path input
- Added try/catch with snackbar error feedback for Load and Browse
- Removed initialDirectory from file picker (could fail in non-sandbox)
- Browse also updates the path input field when a folder is selected
- Signed, notarized, stapled DMG
This commit is contained in:
Mathias Beaulieu-Duncan 2026-04-07 14:01:04 -04:00
parent 53ed5a6cd1
commit f6b496dad7

View File

@ -332,11 +332,24 @@ class _HomeScreenState extends State<HomeScreen> {
} }
Future<void> _pickFolder() async { Future<void> _pickFolder() async {
final result = await FilePicker.platform.getDirectoryPath( try {
dialogTitle: 'Select a folder containing Claude session files', final result = await FilePicker.platform.getDirectoryPath(
); dialogTitle: 'Select a folder containing Claude session files',
if (result != null) { );
await _scanFolder(result); if (result != null) {
_pathController.text = result;
await _scanFolder(result);
}
} catch (e) {
debugPrint('[Browse] Error: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Could not open folder picker: $e'),
backgroundColor: AppColors.error,
),
);
}
} }
} }
@ -393,14 +406,24 @@ class _HomeScreenState extends State<HomeScreen> {
} }
Future<void> _loadSingleFile() async { Future<void> _loadSingleFile() async {
final home = _getRealHome() ?? ''; try {
final result = await FilePicker.platform.pickFiles( final result = await FilePicker.platform.pickFiles(
type: FileType.custom, type: FileType.custom,
allowedExtensions: ['jsonl'], allowedExtensions: ['jsonl'],
initialDirectory: '$home/.claude/projects', );
); if (result != null && result.files.single.path != null) {
if (result != null && result.files.single.path != null) { await _loadFile(result.files.single.path!);
await _loadFile(result.files.single.path!); }
} catch (e) {
debugPrint('[Load] Error: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Could not open file picker: $e'),
backgroundColor: AppColors.error,
),
);
}
} }
} }
@ -491,131 +514,153 @@ class _HomeScreenState extends State<HomeScreen> {
// Header with breadcrumb // Header with breadcrumb
Widget _buildHeader(SessionProvider provider) { Widget _buildHeader(SessionProvider provider) {
return Row( return Column(
children: [ children: [
const Icon(Icons.terminal, size: 24, color: AppColors.assistant), // Row 1: breadcrumb + count
const SizedBox(width: 10), Row(
// Breadcrumb children: [
InkWell( const Icon(Icons.terminal, size: 24, color: AppColors.assistant),
onTap: () => setState(() => _selectedProject = null), const SizedBox(width: 10),
borderRadius: BorderRadius.circular(4), InkWell(
child: Text( onTap: () => setState(() => _selectedProject = null),
'Projects', borderRadius: BorderRadius.circular(4),
style: TextStyle( child: Text(
fontSize: 18, 'Projects',
fontWeight: FontWeight.bold, style: TextStyle(
color: _selectedProject != null fontSize: 18,
? AppColors.assistant fontWeight: FontWeight.bold,
: AppColors.textPrimary, color: _selectedProject != null
decoration: _selectedProject != null ? AppColors.assistant
? TextDecoration.underline : AppColors.textPrimary,
: TextDecoration.none, decoration: _selectedProject != null
decorationColor: AppColors.assistant, ? TextDecoration.underline
: TextDecoration.none,
decorationColor: AppColors.assistant,
),
),
), ),
), if (_selectedProject != null) ...[
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Icon(Icons.chevron_right, size: 18, color: AppColors.textMuted),
),
Flexible(
child: Text(
_selectedProject!.displayName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
overflow: TextOverflow.ellipsis,
),
),
],
const Spacer(),
Text(
_selectedProject != null
? '${_selectedProject!.sessionCount} session${_selectedProject!.sessionCount == 1 ? '' : 's'}'
: '${_projects.length} projects',
style: const TextStyle(fontSize: 12, color: AppColors.textMuted),
),
],
), ),
if (_selectedProject != null) ...[ const SizedBox(height: 12),
const Padding( // Row 2: path input + Load + Browse
padding: EdgeInsets.symmetric(horizontal: 8), Row(
child: Icon(Icons.chevron_right, size: 18, color: AppColors.textMuted), children: [
), // Path input field flexible to fill available space
Flexible( Expanded(
child: Text( child: SizedBox(
_selectedProject!.displayName, height: 36,
style: const TextStyle( child: TextField(
fontSize: 18, controller: _pathController,
fontWeight: FontWeight.bold, focusNode: _pathFocusNode,
color: AppColors.textPrimary, style: const TextStyle(
), fontSize: 12,
overflow: TextOverflow.ellipsis, color: AppColors.textPrimary,
), fontFamily: 'JetBrains Mono',
), ),
], decoration: InputDecoration(
const Spacer(), hintText: '~/.claude/projects',
Text( hintStyle: const TextStyle(
_selectedProject != null fontSize: 12,
? '${_selectedProject!.sessionCount} session${_selectedProject!.sessionCount == 1 ? '' : 's'}' color: AppColors.textMuted,
: '${_projects.length} projects', fontFamily: 'JetBrains Mono',
style: const TextStyle(fontSize: 12, color: AppColors.textMuted), ),
), isDense: true,
// Path input field contentPadding: const EdgeInsets.symmetric(
SizedBox( horizontal: 10,
width: 320, vertical: 8,
child: TextField( ),
controller: _pathController, filled: true,
focusNode: _pathFocusNode, fillColor: AppColors.surface,
style: const TextStyle( border: OutlineInputBorder(
fontSize: 12, borderRadius: BorderRadius.circular(6),
color: AppColors.textPrimary, borderSide: const BorderSide(color: AppColors.surfaceBorder),
fontFamily: 'JetBrains Mono', ),
), enabledBorder: OutlineInputBorder(
decoration: InputDecoration( borderRadius: BorderRadius.circular(6),
hintText: '~/ .claude/projects', borderSide: const BorderSide(color: AppColors.surfaceBorder),
hintStyle: const TextStyle( ),
fontSize: 12, focusedBorder: OutlineInputBorder(
color: AppColors.textMuted, borderRadius: BorderRadius.circular(6),
fontFamily: 'JetBrains Mono', borderSide: const BorderSide(color: AppColors.assistant),
), ),
isDense: true, suffixIcon: IconButton(
contentPadding: const EdgeInsets.symmetric( icon: const Icon(Icons.arrow_forward, size: 14,
horizontal: 10, color: AppColors.textMuted),
vertical: 8, tooltip: 'Scan this folder',
), onPressed: _scanFromPathInput,
filled: true, ),
fillColor: AppColors.surface, ),
border: OutlineInputBorder( onSubmitted: (_) => _scanFromPathInput(),
borderRadius: BorderRadius.circular(6), ),
borderSide: const BorderSide(color: AppColors.surfaceBorder),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: AppColors.surfaceBorder),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: AppColors.assistant),
),
suffixIcon: IconButton(
icon: const Icon(Icons.arrow_forward, size: 14,
color: AppColors.textMuted),
tooltip: 'Scan this folder',
onPressed: _scanFromPathInput,
), ),
), ),
onSubmitted: (_) => _scanFromPathInput(), const SizedBox(width: 8),
), SizedBox(
), height: 36,
const SizedBox(width: 8), child: ElevatedButton.icon(
ElevatedButton.icon( onPressed: _loadSingleFile,
onPressed: _loadSingleFile, icon: const Icon(Icons.insert_drive_file_outlined, size: 14),
icon: const Icon(Icons.insert_drive_file_outlined, size: 14), label: const Text('Load'),
label: const Text('Load'), style: ElevatedButton.styleFrom(
style: ElevatedButton.styleFrom( backgroundColor: AppColors.surfaceLight,
backgroundColor: AppColors.surfaceLight, foregroundColor: AppColors.textPrimary,
foregroundColor: AppColors.textPrimary, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 0),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), minimumSize: Size.zero,
shape: RoundedRectangleBorder( tapTargetSize: MaterialTapTargetSize.shrinkWrap,
borderRadius: BorderRadius.circular(6), shape: RoundedRectangleBorder(
side: const BorderSide(color: AppColors.surfaceBorder), borderRadius: BorderRadius.circular(6),
side: const BorderSide(color: AppColors.surfaceBorder),
),
textStyle: const TextStyle(fontSize: 12),
),
),
), ),
textStyle: const TextStyle(fontSize: 12), const SizedBox(width: 8),
), SizedBox(
), height: 36,
const SizedBox(width: 8), child: ElevatedButton.icon(
ElevatedButton.icon( onPressed: _pickFolder,
onPressed: _pickFolder, icon: const Icon(Icons.folder_open, size: 14),
icon: const Icon(Icons.folder_open, size: 14), label: const Text('Browse'),
label: const Text('Browse'), style: ElevatedButton.styleFrom(
style: ElevatedButton.styleFrom( backgroundColor: AppColors.surfaceLight,
backgroundColor: AppColors.surfaceLight, foregroundColor: AppColors.textPrimary,
foregroundColor: AppColors.textPrimary, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 0),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), minimumSize: Size.zero,
shape: RoundedRectangleBorder( tapTargetSize: MaterialTapTargetSize.shrinkWrap,
borderRadius: BorderRadius.circular(6), shape: RoundedRectangleBorder(
side: const BorderSide(color: AppColors.surfaceBorder), borderRadius: BorderRadius.circular(6),
side: const BorderSide(color: AppColors.surfaceBorder),
),
textStyle: const TextStyle(fontSize: 12),
),
),
), ),
textStyle: const TextStyle(fontSize: 12), ],
),
), ),
], ],
); );