feat: custom native file picker showing hidden files/folders

Replaced file_picker's Load/Browse with a custom NativePickerRegistrar
Swift plugin that opens NSOpenPanel with showsHiddenFiles = true.
The file_picker package hardcodes this to false, making hidden folders
like ~/.claude invisible in its dialogs.

Changes:
- New NativePickerRegistrar.swift: custom NSOpenPanel with hidden files
- New NativePicker Dart service using method channel
- Browse: only shows folders (canChooseFiles=false), hidden visible
- Load: only shows .jsonl files, hidden folders visible
- Registered via AppDelegate.applicationDidFinishLaunching
- Removed file_picker dependency from home_screen imports
- Fixed all info-level lint issues (super params, null-aware, doc comment)
- Signed, notarized, stapled DMG
This commit is contained in:
Mathias Beaulieu-Duncan
2026-04-07 14:32:06 -04:00
parent 0b72c679bc
commit 780ef9378f
6 changed files with 154 additions and 15 deletions
+6 -6
View File
@@ -22,8 +22,8 @@ class ContentBlock {
class TextBlock extends ContentBlock {
final String text;
TextBlock({required this.text, required Map<String, dynamic> raw})
: super(type: 'text', raw: raw);
TextBlock({required this.text, required super.raw})
: super(type: 'text');
factory TextBlock.fromJson(Map<String, dynamic> json) {
return TextBlock(
@@ -40,8 +40,8 @@ class ThinkingBlock extends ContentBlock {
ThinkingBlock({
required this.thinking,
this.signature,
required Map<String, dynamic> raw,
}) : super(type: 'thinking', raw: raw);
required super.raw,
}) : super(type: 'thinking');
factory ThinkingBlock.fromJson(Map<String, dynamic> json) {
return ThinkingBlock(
@@ -63,8 +63,8 @@ class ToolUseBlock extends ContentBlock {
required this.name,
required this.input,
this.linkedResult,
required Map<String, dynamic> raw,
}) : super(type: 'tool_use', raw: raw);
required super.raw,
}) : super(type: 'tool_use');
factory ToolUseBlock.fromJson(Map<String, dynamic> json) {
return ToolUseBlock(
+10 -9
View File
@@ -1,8 +1,8 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../providers/session_provider.dart';
import '../../services/native_picker.dart';
import '../../theme/app_theme.dart';
// ─── Data models ─────────────────────────────────────────────
@@ -120,7 +120,7 @@ class _HomeScreenState extends State<HomeScreen> {
}
/// Returns the real user home directory, even inside App Sandbox.
/// In sandbox, HOME points to ~/Library/Containers/<bundleid>/Data.
/// In sandbox, HOME points to `~/Library/Containers/bundleid/Data`.
String? _getRealHome() {
final home = Platform.environment['HOME'];
if (home == null) return null;
@@ -333,8 +333,9 @@ class _HomeScreenState extends State<HomeScreen> {
Future<void> _pickFolder() async {
try {
final result = await FilePicker.platform.getDirectoryPath(
dialogTitle: 'Select a folder containing Claude session files',
final home = _getRealHome() ?? '';
final result = await NativePicker.getDirectoryPath(
initialDirectory: '$home/.claude/projects',
);
if (result != null) {
_pathController.text = result;
@@ -407,13 +408,13 @@ class _HomeScreenState extends State<HomeScreen> {
Future<void> _loadSingleFile() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
final home = _getRealHome() ?? '';
final result = await NativePicker.pickFile(
allowedExtensions: ['jsonl'],
dialogTitle: 'Select a Claude session file (.jsonl)',
initialDirectory: '$home/.claude/projects',
);
if (result != null && result.files.single.path != null) {
await _loadFile(result.files.single.path!);
if (result != null) {
await _loadFile(result);
}
} catch (e) {
debugPrint('[Load] Error: $e');
+41
View File
@@ -0,0 +1,41 @@
import 'package:flutter/services.dart';
/// Native file picker that shows hidden files/folders on macOS.
/// Uses a custom method channel to NSOpenPanel with showsHiddenFiles = true,
/// which the file_picker package hardcodes to false.
class NativePicker {
static const _channel = MethodChannel('com.svrnty.native_picker');
/// Opens a folder picker that shows hidden directories.
/// Returns the selected folder path, or null if cancelled.
static Future<String?> getDirectoryPath({String? initialDirectory}) async {
try {
return _channel.invokeMethod<String?>('getDirectoryPath', {
'initialDirectory': initialDirectory,
});
} on PlatformException catch (e) {
throw Exception('Native picker error: ${e.message}');
}
}
/// Opens a file picker that shows hidden files, filtered to specific extensions.
/// Returns the selected file path, or null if cancelled.
static Future<String?> pickFile({
List<String> allowedExtensions = const [],
String? initialDirectory,
}) async {
try {
final result = await _channel.invokeMethod<List?>('pickFiles', {
'allowMultiple': false,
'allowedExtensions': allowedExtensions,
'initialDirectory': initialDirectory,
});
if (result != null && result.isNotEmpty) {
return result.first as String;
}
return null;
} on PlatformException catch (e) {
throw Exception('Native picker error: ${e.message}');
}
}
}