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
+22 -13
View File
@@ -2,6 +2,8 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import '../../src/pty/env.dart';
class ToolCheck extends ChangeNotifier {
bool ptycOk = false;
bool pqlOk = false;
@@ -18,24 +20,31 @@ class ToolCheck extends ChangeNotifier {
if (!gitOk) 'git not found',
];
/// Workspace root, set by the app at boot. Falls back to cwd.
static String? workspaceRoot;
Future<void> check() async {
final cwd = Directory.current.path;
ptycOk = File('$cwd/native/linux-x64/ptyc').existsSync() ||
File('$cwd/ptyc/bin/ptyc').existsSync() ||
await _which('ptyc');
pqlOk = await _which('pql');
tmuxOk = await _which('tmux');
gitOk = await _which('git');
final root = workspaceRoot ?? Directory.current.path;
ptycOk = File('$root/native/linux-x64/ptyc').existsSync() ||
File('$root/native/macos-arm64/ptyc').existsSync() ||
File('$root/native/macos-x64/ptyc').existsSync() ||
File('$root/ptyc/bin/ptyc').existsSync() ||
_existsOnPath('ptyc');
pqlOk = _existsOnPath('pql');
tmuxOk = _existsOnPath('tmux');
gitOk = _existsOnPath('git');
checked = true;
notifyListeners();
}
static Future<bool> _which(String name) async {
try {
final r = await Process.run('which', [name]);
return r.exitCode == 0;
} catch (_) {
return false;
/// Check if [name] exists as an executable in any PATH directory.
/// Uses direct file-existence checks — works inside a macOS sandbox
/// without needing to exec `which`.
static bool _existsOnPath(String name) {
for (final dir in expandedPath.split(':')) {
if (dir.isEmpty) continue;
if (File('$dir/$name').existsSync()) return true;
}
return false;
}
}