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
@@ -10,19 +10,19 @@ class ToolStatusItem extends StatelessWidget {
|
||||
final kernel = ClideKernel.of(context);
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return ListenableBuilder(
|
||||
listenable: kernel.toolCheck,
|
||||
listenable: kernel.toolchain,
|
||||
builder: (ctx, _) {
|
||||
final tc = kernel.toolCheck;
|
||||
if (!tc.checked) return const SizedBox.shrink();
|
||||
final tc = kernel.toolchain;
|
||||
if (!tc.resolved) return const SizedBox.shrink();
|
||||
if (tc.allOk) {
|
||||
return _chip('ok', tokens.statusSuccess, tokens);
|
||||
return _chip('application ok', tokens.statusSuccess, tokens);
|
||||
}
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (var i = 0; i < tc.errors.length; i++) ...[
|
||||
for (var i = 0; i < tc.missing.length; i++) ...[
|
||||
if (i > 0) const SizedBox(width: 10),
|
||||
_chip(tc.errors[i], tokens.statusWarning, tokens),
|
||||
_chip('${tc.missing[i]} not found', tokens.statusWarning, tokens),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/services.dart' show MissingPluginException;
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class WelcomeView extends StatelessWidget {
|
||||
@@ -105,7 +106,25 @@ class _StartColumn extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _openFolder(BuildContext context) {
|
||||
void _openFolder(BuildContext context) async {
|
||||
try {
|
||||
final picked = await kernel.window.pickDirectory();
|
||||
if (picked != null) {
|
||||
final ok = await kernel.project.open(picked);
|
||||
if (ok) {
|
||||
kernel.panels.activateTab(Slots.workspace, 'claude.primary');
|
||||
} else {
|
||||
kernel.dialog.show((ctx, dismiss) => _NotARepoDialog(
|
||||
path: picked,
|
||||
onDismiss: () => dismiss(),
|
||||
));
|
||||
}
|
||||
}
|
||||
return;
|
||||
} on MissingPluginException {
|
||||
// Platform has no native picker — fall through to text dialog.
|
||||
}
|
||||
|
||||
kernel.dialog.show<String>((ctx, dismiss) {
|
||||
return _OpenProjectDialog(
|
||||
onOpen: (path) async {
|
||||
@@ -242,20 +261,20 @@ class _StatusLine extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final themeName = kernel.theme.currentName;
|
||||
return ListenableBuilder(
|
||||
listenable: kernel.toolCheck,
|
||||
listenable: kernel.toolchain,
|
||||
builder: (ctx, _) {
|
||||
final tc = kernel.toolCheck;
|
||||
final tc = kernel.toolchain;
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ClideText('clide 2.0.0-dev', muted: true, fontSize: 12, fontFamily: clideMonoFamily),
|
||||
ClideText(' · ', muted: true, fontSize: 12),
|
||||
if (!tc.checked)
|
||||
if (!tc.resolved)
|
||||
ClideText('checking…', muted: true, fontSize: 12, fontFamily: clideMonoFamily)
|
||||
else if (tc.allOk)
|
||||
ClideText('application ok', fontSize: 12, fontFamily: clideMonoFamily, color: tokens.statusSuccess)
|
||||
else
|
||||
ClideText(tc.errors.join(' · '), fontSize: 12, fontFamily: clideMonoFamily, color: tokens.statusWarning),
|
||||
ClideText(tc.missing.map((t) => '$t not found').join(' · '), fontSize: 12, fontFamily: clideMonoFamily, color: tokens.statusWarning),
|
||||
ClideText(' · ', muted: true, fontSize: 12),
|
||||
_ThemeLink(tokens: tokens, kernel: kernel, themeName: themeName),
|
||||
],
|
||||
@@ -379,3 +398,45 @@ class _OpenProjectDialogState extends State<_OpenProjectDialog> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NotARepoDialog extends StatelessWidget {
|
||||
const _NotARepoDialog({required this.path, required this.onDismiss});
|
||||
final String path;
|
||||
final VoidCallback onDismiss;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return Container(
|
||||
width: 420,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.modalSurfaceBackground,
|
||||
border: Border.all(color: tokens.modalSurfaceBorder),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const ClideText('No git repo found', fontSize: 16, fontWeight: FontWeight.w600),
|
||||
const SizedBox(height: 8),
|
||||
ClideText(path, muted: true, fontSize: 13),
|
||||
const SizedBox(height: 8),
|
||||
const ClideText(
|
||||
'A clide project root requires a git repository.',
|
||||
muted: true,
|
||||
fontSize: 13,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ClideButton(label: 'OK', onPressed: () => onDismiss()),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user