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
+12 -4
View File
@@ -15,10 +15,11 @@ import '../ipc/envelope.dart';
import '../ipc/schema_v1.dart';
import '../panes/pane.dart';
import '../panes/registry.dart';
import '../../kernel/src/toolchain.dart';
import 'dispatcher.dart';
void registerPaneCommands(DaemonDispatcher d, PaneRegistry registry, {String defaultPtycPath = 'ptyc'}) {
d.register('pane.spawn', (req) => _spawn(req, registry, defaultPtycPath));
void registerPaneCommands(DaemonDispatcher d, PaneRegistry registry, {required Toolchain toolchain}) {
d.register('pane.spawn', (req) => _spawn(req, registry, toolchain));
d.register('pane.list', (req) => _list(req, registry));
d.register('pane.close', (req) => _close(req, registry));
d.register('pane.write', (req) => _write(req, registry));
@@ -47,7 +48,14 @@ IpcResponse _notFound(String id, String message) => IpcResponse.err(
),
);
Future<IpcResponse> _spawn(IpcRequest req, PaneRegistry registry, String defaultPtycPath) async {
Future<IpcResponse> _spawn(IpcRequest req, PaneRegistry registry, Toolchain toolchain) async {
// Wait for toolchain resolution if it hasn't completed yet.
if (!toolchain.resolved) {
await Future.any([
toolchain.waitForResolution(),
Future.delayed(const Duration(seconds: 5)),
]);
}
final args = req.args;
final rawArgv = args['argv'];
if (rawArgv is! List || rawArgv.isEmpty) {
@@ -84,7 +92,7 @@ Future<IpcResponse> _spawn(IpcRequest req, PaneRegistry registry, String default
cols: (args['cols'] as num?)?.toInt() ?? 80,
rows: (args['rows'] as num?)?.toInt() ?? 24,
title: args['title'] as String?,
ptycPath: (args['ptyc_path'] as String?) ?? defaultPtycPath,
ptycPath: (args['ptyc_path'] as String?) ?? toolchain.ptyc,
);
return IpcResponse.ok(id: req.id, data: pane.toJson());
} catch (e) {