feat(tools): Tools settings category + re-detect for supporter binaries (T-495)
ToolsSettingsExtension adds a Tools settings category — a path field per tool (app.tools.<name>) plus a Re-detect action (tools.detect) — and keeps the live resolver in sync as paths are edited (supporterBinariesFrom). en + nl catalogs. Completes the D-104 UI surface. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- **Tool path resolution + settings.** clide resolves supporter binaries (claude,
|
||||||
|
d2) via an explicit per-tool path, else PATH and the common install dirs — now
|
||||||
|
including Homebrew-on-Linux. A Tools settings category edits the paths and
|
||||||
|
re-detects; a broken path shows in Problems. (T-495, D-104)
|
||||||
- **Check for updates (About box).** Help → About has a manual "Check for
|
- **Check for updates (About box).** Help → About has a manual "Check for
|
||||||
updates" button that compares your version to the latest GitHub release and
|
updates" button that compares your version to the latest GitHub release and
|
||||||
links to the notes — explicit and user-initiated, no background polling (the
|
links to the notes — explicit and user-initiated, no background polling (the
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"command.detect": { "translation": "Re-detect tool paths" },
|
||||||
|
"settings.title": { "translation": "Tools" },
|
||||||
|
"settings.section.binaries": { "translation": "Supporter binaries" },
|
||||||
|
"settings.field.claude.label": { "translation": "Claude CLI" },
|
||||||
|
"settings.field.claude.help": { "translation": "Absolute path to the claude binary; blank to auto-resolve." },
|
||||||
|
"settings.field.d2.label": { "translation": "d2" },
|
||||||
|
"settings.field.d2.help": { "translation": "Absolute path to the d2 diagram compiler; blank to auto-resolve." },
|
||||||
|
"settings.field.detect.label": { "translation": "Re-detect" },
|
||||||
|
"settings.field.detect.help": { "translation": "Re-scan PATH and the common install dirs, overwriting the paths above." }
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"command.detect": { "translation": "Tool-paden opnieuw detecteren" },
|
||||||
|
"settings.title": { "translation": "Tools" },
|
||||||
|
"settings.section.binaries": { "translation": "Hulpprogramma's" },
|
||||||
|
"settings.field.claude.label": { "translation": "Claude-CLI" },
|
||||||
|
"settings.field.claude.help": { "translation": "Absoluut pad naar het claude-binary; leeg om automatisch te bepalen." },
|
||||||
|
"settings.field.d2.label": { "translation": "d2" },
|
||||||
|
"settings.field.d2.help": { "translation": "Absoluut pad naar de d2-diagramcompiler; leeg om automatisch te bepalen." },
|
||||||
|
"settings.field.detect.label": { "translation": "Opnieuw detecteren" },
|
||||||
|
"settings.field.detect.help": { "translation": "Scan PATH en de gangbare installatiemappen opnieuw; overschrijft de paden hierboven." }
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import 'package:clide/clide.dart';
|
||||||
|
import 'package:clide/extension/extension.dart';
|
||||||
|
import 'package:clide/kernel/kernel.dart';
|
||||||
|
import 'package:clide/src/env/supporter_binaries.dart';
|
||||||
|
|
||||||
|
/// Settings surface for the external supporter binaries clide shells out to —
|
||||||
|
/// claude, d2, … (D-104 / T-495). A path field per tool (`app.tools.<name>`),
|
||||||
|
/// auto-detected on first run, plus a Re-detect action. Editing a path rebuilds
|
||||||
|
/// the live resolver so the change takes effect without a restart.
|
||||||
|
class ToolsSettingsExtension extends ClideExtension {
|
||||||
|
@override
|
||||||
|
String get id => 'builtin.tools-settings';
|
||||||
|
@override
|
||||||
|
String get title => 'Tool paths';
|
||||||
|
@override
|
||||||
|
String get version => '0.1.0';
|
||||||
|
|
||||||
|
ClideExtensionContext? _ctx;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> activate(ClideExtensionContext ctx) async {
|
||||||
|
_ctx = ctx;
|
||||||
|
ctx.settings.addListener(_sync);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deactivate() async {
|
||||||
|
_ctx?.settings.removeListener(_sync);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebuild the process-wide resolver from the current override keys so a path
|
||||||
|
/// edited in the settings panel applies live. Cheap — reads a couple of keys.
|
||||||
|
void _sync() {
|
||||||
|
final ctx = _ctx;
|
||||||
|
if (ctx == null) return;
|
||||||
|
activeSupporterBinaries = supporterBinariesFrom((k) => ctx.settings.get<Object>(k));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<ContributionPoint> get contributions => [
|
||||||
|
CommandContribution(
|
||||||
|
id: 'tools.detect',
|
||||||
|
command: 'tools.detect',
|
||||||
|
title: 'Re-detect tool paths',
|
||||||
|
titleKey: 'command.detect',
|
||||||
|
i18nNamespace: id,
|
||||||
|
run: _redetect,
|
||||||
|
),
|
||||||
|
SettingsCategoryContribution(
|
||||||
|
id: 'tools',
|
||||||
|
category: SettingsCategory(
|
||||||
|
id: 'tools',
|
||||||
|
title: 'Tools',
|
||||||
|
titleKey: 'settings.title',
|
||||||
|
i18nNamespace: id,
|
||||||
|
iconName: 'wrench',
|
||||||
|
priority: 60,
|
||||||
|
sections: [
|
||||||
|
SettingsSection(
|
||||||
|
label: 'Supporter binaries',
|
||||||
|
labelKey: 'settings.section.binaries',
|
||||||
|
fields: [
|
||||||
|
SettingsField(
|
||||||
|
key: supporterToolKey('claude'),
|
||||||
|
kind: SettingsFieldKind.text,
|
||||||
|
label: 'Claude CLI',
|
||||||
|
labelKey: 'settings.field.claude.label',
|
||||||
|
help: 'Absolute path to the claude binary; blank to auto-resolve.',
|
||||||
|
helpKey: 'settings.field.claude.help',
|
||||||
|
),
|
||||||
|
SettingsField(
|
||||||
|
key: supporterToolKey('d2'),
|
||||||
|
kind: SettingsFieldKind.text,
|
||||||
|
label: 'd2',
|
||||||
|
labelKey: 'settings.field.d2.label',
|
||||||
|
help: 'Absolute path to the d2 diagram compiler; blank to auto-resolve.',
|
||||||
|
helpKey: 'settings.field.d2.help',
|
||||||
|
),
|
||||||
|
SettingsField(
|
||||||
|
key: 'app.tools.redetect',
|
||||||
|
kind: SettingsFieldKind.file,
|
||||||
|
label: 'Re-detect',
|
||||||
|
labelKey: 'settings.field.detect.label',
|
||||||
|
help: 'Re-scan PATH and the common install dirs, overwriting the paths above.',
|
||||||
|
helpKey: 'settings.field.detect.help',
|
||||||
|
fileCommand: 'tools.detect',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
Future<IpcResponse> _redetect(List<String> args) async {
|
||||||
|
final ctx = _ctx;
|
||||||
|
if (ctx == null) {
|
||||||
|
return IpcResponse.err(
|
||||||
|
id: '',
|
||||||
|
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'tools-settings not activated'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final resolver = await redetectSupporterBinaries(write: (k, v) => ctx.settings.set<Object?>(k, v));
|
||||||
|
activeSupporterBinaries = resolver;
|
||||||
|
return IpcResponse.ok(
|
||||||
|
id: '',
|
||||||
|
data: {
|
||||||
|
'detected': {for (final t in knownSupporterTools) t: resolver.resolve(t)},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export 'src/extension.dart';
|
||||||
@@ -27,6 +27,7 @@ import 'package:clide/builtin/problems/problems.dart';
|
|||||||
import 'package:clide/builtin/settings_ui/settings_ui.dart';
|
import 'package:clide/builtin/settings_ui/settings_ui.dart';
|
||||||
import 'package:clide/builtin/terminal/terminal.dart';
|
import 'package:clide/builtin/terminal/terminal.dart';
|
||||||
import 'package:clide/builtin/theme_picker/theme_picker.dart';
|
import 'package:clide/builtin/theme_picker/theme_picker.dart';
|
||||||
|
import 'package:clide/builtin/tools_settings/tools_settings.dart';
|
||||||
import 'package:clide/builtin/view/view.dart';
|
import 'package:clide/builtin/view/view.dart';
|
||||||
import 'package:clide/builtin/vim/vim.dart';
|
import 'package:clide/builtin/vim/vim.dart';
|
||||||
import 'package:clide/builtin/tickets/tickets.dart';
|
import 'package:clide/builtin/tickets/tickets.dart';
|
||||||
@@ -565,6 +566,7 @@ Future<void> main() async {
|
|||||||
..register(ExtensionsUiExtension())
|
..register(ExtensionsUiExtension())
|
||||||
..register(KeybindingsUiExtension())
|
..register(KeybindingsUiExtension())
|
||||||
..register(ClaudeControlExtension())
|
..register(ClaudeControlExtension())
|
||||||
|
..register(ToolsSettingsExtension())
|
||||||
..register(CliInstallExtension());
|
..register(CliInstallExtension());
|
||||||
|
|
||||||
await services.extensions.activateAll();
|
await services.extensions.activateAll();
|
||||||
|
|||||||
Vendored
+6
@@ -149,6 +149,12 @@ Future<SupporterBinaries> redetectSupporterBinaries({
|
|||||||
return SupporterBinaries(overrides: fresh);
|
return SupporterBinaries(overrides: fresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A resolver built from the current per-tool override keys, with no detect or
|
||||||
|
/// marker side effects — for rebuilding [activeSupporterBinaries] live when a
|
||||||
|
/// path is edited in settings.
|
||||||
|
SupporterBinaries supporterBinariesFrom(Object? Function(String key) read, {List<String> tools = knownSupporterTools}) =>
|
||||||
|
SupporterBinaries(overrides: _readOverrides(read, tools));
|
||||||
|
|
||||||
Map<String, String> _readOverrides(Object? Function(String) read, List<String> tools) {
|
Map<String, String> _readOverrides(Object? Function(String) read, List<String> tools) {
|
||||||
final overrides = <String, String>{};
|
final overrides = <String, String>{};
|
||||||
for (final t in tools) {
|
for (final t in tools) {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/// T-495: ToolsSettingsExtension contributes the Tools settings category and
|
||||||
|
/// the tools.detect command, and keeps the live resolver in sync with edits.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:clide/builtin/tools_settings/tools_settings.dart';
|
||||||
|
import 'package:clide/extension/extension.dart';
|
||||||
|
import 'package:clide/src/env/supporter_binaries.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../../helpers/kernel_fixture.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late KernelFixture f;
|
||||||
|
SupporterBinaries? saved;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
saved = activeSupporterBinaries;
|
||||||
|
f = await KernelFixture.create();
|
||||||
|
f.services.extensions.register(ToolsSettingsExtension());
|
||||||
|
await f.services.extensions.activate('builtin.tools-settings');
|
||||||
|
});
|
||||||
|
tearDown(() {
|
||||||
|
activeSupporterBinaries = saved; // un-pollute the process-wide resolver
|
||||||
|
f.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('contributes the Tools settings category and the tools.detect command', () {
|
||||||
|
final ext = ToolsSettingsExtension();
|
||||||
|
final cats = ext.contributions.whereType<SettingsCategoryContribution>();
|
||||||
|
expect(cats.any((c) => c.category.id == 'tools'), isTrue);
|
||||||
|
expect(f.services.commands.get('tools.detect'), isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('editing a tool path in settings rebuilds the live resolver', () async {
|
||||||
|
await f.services.settings.set<String>(supporterToolKey('d2'), '/custom/d2');
|
||||||
|
// The listener rebuilt activeSupporterBinaries from the keys; /custom/d2
|
||||||
|
// isn't a real file, so it reads as a stale pin — proving the edit took.
|
||||||
|
expect(activeSupporterBinaries?.isStalePin('d2'), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tools.detect runs, marks first-run done, and refreshes the resolver', () async {
|
||||||
|
final r = await f.services.commands.execute('tools.detect');
|
||||||
|
expect(r.ok, isTrue);
|
||||||
|
expect(f.services.settings.get<bool>('app.tools.detected'), isTrue);
|
||||||
|
expect(activeSupporterBinaries, isNotNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
+13
@@ -105,4 +105,17 @@ void main() {
|
|||||||
expect(store['app.tools.detected'], isTrue);
|
expect(store['app.tools.detected'], isTrue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('supporterBinariesFrom', () {
|
||||||
|
test('builds a resolver from the override keys', () {
|
||||||
|
final store = <String, Object?>{'app.tools.d2': '/x/d2'};
|
||||||
|
final r = supporterBinariesFrom((k) => store[k], tools: ['d2']);
|
||||||
|
expect(r.isStalePin('d2'), isTrue); // /x/d2 not a real file → loaded as override
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ignores blank or missing keys', () {
|
||||||
|
final store = <String, Object?>{'app.tools.d2': ''};
|
||||||
|
expect(supporterBinariesFrom((k) => store[k], tools: ['claude', 'd2']).isStalePin('d2'), isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user