add Toolchain, GitClient, native directory picker

Toolchain centralizes binary resolution — replaces five ad-hoc
mechanisms (expandedPath, _resolveGit, _resolve, _resolvePtyc,
_existsOnPath). Resolves via Future.delayed after runApp to avoid
blocking the merged UI/platform thread on macOS.

GitClient wraps all git operations with a typed API. Every subprocess
call goes through _run() using toolchain.git + toolchain.gitEnv.
Replaces free functions in operations.dart.

Native directory picker: NSOpenPanel on macOS (method channel in
AppDelegate), GtkFileChooserDialog on Linux. Falls back to text-input
dialog on web or MissingPluginException. Shows "No git repo found"
dialog when the selected directory is not a git repository.

PqlClient and pane commands updated to use Toolchain. ToolCheck
replaced by Toolchain.missing/allOk. All IPC handlers now catch
GitException to prevent unhandled exceptions on the merged thread.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeroen Schweitzer
2026-04-25 13:18:39 +02:00
co-authored by Claude Opus 4.6
parent 8edcc78bfe
commit 73e80a55a6
22 changed files with 734 additions and 147 deletions
+18 -7
View File
@@ -8,6 +8,8 @@ library;
import 'dart:convert';
import 'dart:io';
import '../../kernel/src/toolchain.dart';
class PqlException implements Exception {
const PqlException(this.message, {this.exitCode = 1, this.stderr = ''});
final String message;
@@ -19,10 +21,10 @@ class PqlException implements Exception {
}
class PqlClient {
PqlClient({required this.workDir, this.pqlBinary = 'pql'});
PqlClient({required this.workDir, required this.toolchain});
final Directory workDir;
final String pqlBinary;
final Toolchain toolchain;
Future<List<Map<String, Object?>>> files({String? glob, int? limit}) async {
final args = ['files'];
@@ -165,11 +167,20 @@ class PqlClient {
}
Future<Object?> _run(List<String> args) async {
final r = await Process.run(
pqlBinary,
args,
workingDirectory: workDir.path,
);
final ProcessResult r;
try {
r = await Process.run(
toolchain.pql,
args,
workingDirectory: workDir.path,
);
} on ProcessException catch (e) {
throw PqlException(
'pql ${args.first}: ${e.message}',
exitCode: e.errorCode,
stderr: e.toString(),
);
}
final stderr = (r.stderr as String).trim();
// Exit 2 = zero matches — valid empty result, not an error.
if (r.exitCode != 0 && r.exitCode != 2) {