diff --git a/CHANGELOG.md b/CHANGELOG.md index bfa3c791..2b58f514 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### 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 updates" button that compares your version to the latest GitHub release and links to the notes — explicit and user-initiated, no background polling (the diff --git a/assets/i18n/en_us/builtin.tools-settings.json b/assets/i18n/en_us/builtin.tools-settings.json new file mode 100644 index 00000000..c4e71379 --- /dev/null +++ b/assets/i18n/en_us/builtin.tools-settings.json @@ -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." } +} diff --git a/assets/i18n/nl_nl/builtin.tools-settings.json b/assets/i18n/nl_nl/builtin.tools-settings.json new file mode 100644 index 00000000..12f1ad9a --- /dev/null +++ b/assets/i18n/nl_nl/builtin.tools-settings.json @@ -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." } +} diff --git a/lib/builtin/tools_settings/src/extension.dart b/lib/builtin/tools_settings/src/extension.dart new file mode 100644 index 00000000..5017e263 --- /dev/null +++ b/lib/builtin/tools_settings/src/extension.dart @@ -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.`), +/// 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 activate(ClideExtensionContext ctx) async { + _ctx = ctx; + ctx.settings.addListener(_sync); + } + + @override + Future 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(k)); + } + + @override + List 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 _redetect(List 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(k, v)); + activeSupporterBinaries = resolver; + return IpcResponse.ok( + id: '', + data: { + 'detected': {for (final t in knownSupporterTools) t: resolver.resolve(t)}, + }, + ); + } +} diff --git a/lib/builtin/tools_settings/tools_settings.dart b/lib/builtin/tools_settings/tools_settings.dart new file mode 100644 index 00000000..b968b883 --- /dev/null +++ b/lib/builtin/tools_settings/tools_settings.dart @@ -0,0 +1 @@ +export 'src/extension.dart'; diff --git a/lib/main.dart b/lib/main.dart index 01c7d3b4..729e6b5f 100644 --- a/lib/main.dart +++ b/lib/main.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/terminal/terminal.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/vim/vim.dart'; import 'package:clide/builtin/tickets/tickets.dart'; @@ -565,6 +566,7 @@ Future main() async { ..register(ExtensionsUiExtension()) ..register(KeybindingsUiExtension()) ..register(ClaudeControlExtension()) + ..register(ToolsSettingsExtension()) ..register(CliInstallExtension()); await services.extensions.activateAll(); diff --git a/lib/src/env/supporter_binaries.dart b/lib/src/env/supporter_binaries.dart index 8a2fef4b..53369878 100644 --- a/lib/src/env/supporter_binaries.dart +++ b/lib/src/env/supporter_binaries.dart @@ -149,6 +149,12 @@ Future redetectSupporterBinaries({ 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 tools = knownSupporterTools}) => + SupporterBinaries(overrides: _readOverrides(read, tools)); + Map _readOverrides(Object? Function(String) read, List tools) { final overrides = {}; for (final t in tools) { diff --git a/test/builtin/tools_settings/extension_test.dart b/test/builtin/tools_settings/extension_test.dart new file mode 100644 index 00000000..4c69c9af --- /dev/null +++ b/test/builtin/tools_settings/extension_test.dart @@ -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(); + 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(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('app.tools.detected'), isTrue); + expect(activeSupporterBinaries, isNotNull); + }); +} diff --git a/test/src/env/supporter_binaries_test.dart b/test/src/env/supporter_binaries_test.dart index 23829465..4bc7ada9 100644 --- a/test/src/env/supporter_binaries_test.dart +++ b/test/src/env/supporter_binaries_test.dart @@ -105,4 +105,17 @@ void main() { expect(store['app.tools.detected'], isTrue); }); }); + + group('supporterBinariesFrom', () { + test('builds a resolver from the override keys', () { + final store = {'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 = {'app.tools.d2': ''}; + expect(supporterBinariesFrom((k) => store[k], tools: ['claude', 'd2']).isStalePin('d2'), isFalse); + }); + }); }