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:
co-authored by
Claude Opus 4.6
parent
8edcc78bfe
commit
73e80a55a6
@@ -5,11 +5,8 @@
|
||||
/// can refresh.
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import '../git/diff.dart';
|
||||
import '../git/operations.dart';
|
||||
import '../git/status.dart';
|
||||
import '../git/client.dart';
|
||||
import '../git/operations.dart' show GitException;
|
||||
import '../ipc/envelope.dart';
|
||||
import '../ipc/schema_v1.dart';
|
||||
import '../panes/event_sink.dart';
|
||||
@@ -17,22 +14,30 @@ import 'dispatcher.dart';
|
||||
|
||||
void registerGitCommands(
|
||||
DaemonDispatcher d,
|
||||
Directory workDir,
|
||||
GitClient git,
|
||||
DaemonEventSink events,
|
||||
) {
|
||||
d.register('git.status', (req) async {
|
||||
final status = await gitStatus(workDir);
|
||||
return IpcResponse.ok(id: req.id, data: status.toJson());
|
||||
try {
|
||||
final status = await git.status();
|
||||
return IpcResponse.ok(id: req.id, data: status.toJson());
|
||||
} on GitException catch (e) {
|
||||
return _gitError(req.id, e);
|
||||
}
|
||||
});
|
||||
|
||||
d.register('git.diff', (req) async {
|
||||
final staged = req.args['staged'] as bool? ?? false;
|
||||
final paths = _pathList(req.args['paths']);
|
||||
final diffs = await gitDiff(workDir, staged: staged, paths: paths);
|
||||
return IpcResponse.ok(id: req.id, data: {
|
||||
'staged': staged,
|
||||
'diffs': [for (final d in diffs) d.toJson()],
|
||||
});
|
||||
try {
|
||||
final staged = req.args['staged'] as bool? ?? false;
|
||||
final paths = _pathList(req.args['paths']);
|
||||
final diffs = await git.diff(staged: staged, paths: paths);
|
||||
return IpcResponse.ok(id: req.id, data: {
|
||||
'staged': staged,
|
||||
'diffs': [for (final d in diffs) d.toJson()],
|
||||
});
|
||||
} on GitException catch (e) {
|
||||
return _gitError(req.id, e);
|
||||
}
|
||||
});
|
||||
|
||||
d.register('git.stage', (req) async {
|
||||
@@ -49,7 +54,7 @@ void registerGitCommands(
|
||||
);
|
||||
}
|
||||
try {
|
||||
await gitStage(workDir, paths);
|
||||
await git.stage(paths);
|
||||
_emitChanged(events);
|
||||
return IpcResponse.ok(id: req.id, data: {'staged': paths});
|
||||
} on GitException catch (e) {
|
||||
@@ -59,7 +64,7 @@ void registerGitCommands(
|
||||
|
||||
d.register('git.stage-all', (req) async {
|
||||
try {
|
||||
await gitStage(workDir, const []);
|
||||
await git.stage(const []);
|
||||
_emitChanged(events);
|
||||
return IpcResponse.ok(id: req.id, data: const {'staged': 'all'});
|
||||
} on GitException catch (e) {
|
||||
@@ -70,7 +75,7 @@ void registerGitCommands(
|
||||
d.register('git.unstage', (req) async {
|
||||
final paths = _pathList(req.args['paths']);
|
||||
try {
|
||||
await gitUnstage(workDir, paths);
|
||||
await git.unstage(paths);
|
||||
_emitChanged(events);
|
||||
return IpcResponse.ok(id: req.id, data: {'unstaged': paths});
|
||||
} on GitException catch (e) {
|
||||
@@ -91,7 +96,7 @@ void registerGitCommands(
|
||||
);
|
||||
}
|
||||
try {
|
||||
await gitStageHunk(workDir, patch);
|
||||
await git.stageHunk(patch);
|
||||
_emitChanged(events);
|
||||
return IpcResponse.ok(id: req.id, data: const {'applied': true});
|
||||
} on GitException catch (e) {
|
||||
@@ -112,7 +117,7 @@ void registerGitCommands(
|
||||
);
|
||||
}
|
||||
try {
|
||||
await gitUnstageHunk(workDir, patch);
|
||||
await git.unstageHunk(patch);
|
||||
_emitChanged(events);
|
||||
return IpcResponse.ok(id: req.id, data: const {'applied': true});
|
||||
} on GitException catch (e) {
|
||||
@@ -133,7 +138,7 @@ void registerGitCommands(
|
||||
);
|
||||
}
|
||||
try {
|
||||
await gitDiscard(workDir, paths);
|
||||
await git.discard(paths);
|
||||
_emitChanged(events);
|
||||
return IpcResponse.ok(id: req.id, data: {'discarded': paths});
|
||||
} on GitException catch (e) {
|
||||
@@ -154,7 +159,7 @@ void registerGitCommands(
|
||||
);
|
||||
}
|
||||
try {
|
||||
final hash = await gitCommit(workDir, message);
|
||||
final hash = await git.commit(message);
|
||||
_emitChanged(events);
|
||||
return IpcResponse.ok(id: req.id, data: {'hash': hash});
|
||||
} on GitException catch (e) {
|
||||
@@ -166,7 +171,7 @@ void registerGitCommands(
|
||||
final message = req.args['message'] as String?;
|
||||
final includeUntracked = req.args['includeUntracked'] as bool? ?? false;
|
||||
try {
|
||||
await gitStash(workDir, message: message, includeUntracked: includeUntracked);
|
||||
await git.stash(message: message, includeUntracked: includeUntracked);
|
||||
_emitChanged(events);
|
||||
return IpcResponse.ok(id: req.id, data: const {'stashed': true});
|
||||
} on GitException catch (e) {
|
||||
@@ -176,7 +181,7 @@ void registerGitCommands(
|
||||
|
||||
d.register('git.stash-pop', (req) async {
|
||||
try {
|
||||
await gitStashPop(workDir);
|
||||
await git.stashPop();
|
||||
_emitChanged(events);
|
||||
return IpcResponse.ok(id: req.id, data: const {'popped': true});
|
||||
} on GitException catch (e) {
|
||||
@@ -185,16 +190,20 @@ void registerGitCommands(
|
||||
});
|
||||
|
||||
d.register('git.log', (req) async {
|
||||
final count = (req.args['count'] as num?)?.toInt() ?? 20;
|
||||
final entries = await gitLog(workDir, count: count);
|
||||
return IpcResponse.ok(id: req.id, data: {
|
||||
'entries': [for (final e in entries) e.toJson()],
|
||||
});
|
||||
try {
|
||||
final count = (req.args['count'] as num?)?.toInt() ?? 20;
|
||||
final entries = await git.log(count: count);
|
||||
return IpcResponse.ok(id: req.id, data: {
|
||||
'entries': [for (final e in entries) e.toJson()],
|
||||
});
|
||||
} on GitException catch (e) {
|
||||
return _gitError(req.id, e);
|
||||
}
|
||||
});
|
||||
|
||||
d.register('git.pull', (req) async {
|
||||
try {
|
||||
final output = await gitPull(workDir);
|
||||
final output = await git.pull();
|
||||
_emitChanged(events);
|
||||
return IpcResponse.ok(id: req.id, data: {'output': output});
|
||||
} on GitException catch (e) {
|
||||
@@ -207,12 +216,7 @@ void registerGitCommands(
|
||||
final branch = req.args['branch'] as String?;
|
||||
final setUpstream = req.args['setUpstream'] as bool? ?? false;
|
||||
try {
|
||||
final output = await gitPush(
|
||||
workDir,
|
||||
remote: remote,
|
||||
branch: branch,
|
||||
setUpstream: setUpstream,
|
||||
);
|
||||
final output = await git.push(remote: remote, branch: branch, setUpstream: setUpstream);
|
||||
return IpcResponse.ok(id: req.id, data: {'output': output});
|
||||
} on GitException catch (e) {
|
||||
return _gitError(req.id, e);
|
||||
@@ -220,13 +224,14 @@ void registerGitCommands(
|
||||
});
|
||||
|
||||
d.register('git.branches', (req) async {
|
||||
final branches = await gitBranches(workDir);
|
||||
return IpcResponse.ok(id: req.id, data: {
|
||||
'branches': [
|
||||
for (final b in branches)
|
||||
{'name': b.name, 'current': b.current},
|
||||
],
|
||||
});
|
||||
try {
|
||||
final b = await git.branches();
|
||||
return IpcResponse.ok(id: req.id, data: {
|
||||
'branches': [for (final e in b) {'name': e.name, 'current': e.current}],
|
||||
});
|
||||
} on GitException catch (e) {
|
||||
return _gitError(req.id, e);
|
||||
}
|
||||
});
|
||||
|
||||
d.register('git.checkout', (req) async {
|
||||
@@ -242,7 +247,7 @@ void registerGitCommands(
|
||||
);
|
||||
}
|
||||
try {
|
||||
await gitCheckout(workDir, branch);
|
||||
await git.checkout(branch);
|
||||
_emitChanged(events);
|
||||
return IpcResponse.ok(id: req.id, data: {'branch': branch});
|
||||
} on GitException catch (e) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user