From 79c5294d448b3d1d0a668fb29097325e078e5716 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 17 Jun 2026 13:45:01 +0200 Subject: [PATCH] =?UTF-8?q?feat(settings):=20Extensions=20tab=20=E2=80=94?= =?UTF-8?q?=20"watch=20this=20space"=20notice=20(T-456)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Built-in extensions are always on and there's no third-party install path yet, so the Extensions tab is a notice rather than a toggle list — a toggle list would let users brick the app by disabling load-bearing builtins. It explains that installing/enabling/disabling arrives with third-party (Lua) extensions and points at the records that pick it up (D-16 / T-8). Fleshes out the builtin.extensions-ui stub via the custom-control hatch. Adds two renderer guards — empty section/field labels render no chrome — so a notice-only category shows just its card. Tests: contributes the Extensions category + notice control; the notice renders. Co-Authored-By: Claude Opus 4.8 (1M context) --- .pql/changelog/ticket_history/2026-06.sql | 2 + .pql/changelog/tickets/2026-06.sql | 2 + CHANGELOG.md | 4 ++ lib/builtin/extensions_ui/extensions_ui.dart | 1 + lib/builtin/extensions_ui/src/extension.dart | 32 +++++++++-- .../extensions_ui/src/extensions_notice.dart | 56 +++++++++++++++++++ .../src/settings_category_view.dart | 12 ++-- .../catalog/builtin.extensions-ui_en_us.json | 5 ++ .../builtin/extensions_ui/extension_test.dart | 31 ++++++++++ 9 files changed, 134 insertions(+), 11 deletions(-) create mode 100644 lib/builtin/extensions_ui/src/extensions_notice.dart create mode 100644 lib/kernel/src/i18n/catalog/builtin.extensions-ui_en_us.json create mode 100644 test/builtin/extensions_ui/extension_test.dart diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 9470e499..34fde6e7 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -5911,3 +5911,5 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD0A0ZTS9JZCSQ4W8ZVEH2VR', 'status', 'in_progress', 'done', NULL, '2026-06-17 10:48:30', '2026-06-17 10:48:30.719', '2026-06-17 10:48:30.719', NULL, '3758ec372c9635bfd7756a8939f4b329', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD0A4MY8DF6NB0KNV96RQADR', 'status', 'backlog', 'in_progress', NULL, '2026-06-17 10:56:30', '2026-06-17 10:56:30.289', '2026-06-17 10:56:30.289', NULL, 'f4ca163e49ee68804c6d66bc059a6dfd', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD0A4MY8DF6NB0KNV96RQADR', 'status', 'in_progress', 'done', NULL, '2026-06-17 11:04:11', '2026-06-17 11:04:11.562', '2026-06-17 11:04:11.562', NULL, '10855e5b574013427ec9a6872c674d1d', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15TY946689VZK3MNABSMDW', 'status', 'backlog', 'in_progress', NULL, '2026-06-17 11:21:19', '2026-06-17 11:21:19.581', '2026-06-17 11:21:19.581', NULL, '02d53470150602dbbcaecc165a4a3d63', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15TY946689VZK3MNABSMDW', 'status', 'in_progress', 'done', NULL, '2026-06-17 11:43:37', '2026-06-17 11:43:37.615', '2026-06-17 11:43:37.615', NULL, 'a2f67644a293f12da9b0cdf28dc79055', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 36e21cb8..54ac954c 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -7179,3 +7179,5 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD0A0ZTS9JZCSQ4W8ZVEH2VR', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Keymap settings category (preset select + options)', 'Register the keymap settings schema: preset enum select (default / vscode / jetbrains / vim — T-115, T-64, T-65, T-66) wired to KeymapService.setPreset, plus related options (e.g. sequence-timeout). Reuses the schema field renderer (T-448).', 'done', 'medium', NULL, NULL, NULL, '2026-06-16 11:19:11', '2026-06-17 10:48:30.719', NULL, '4678d116aadfaea65526200caf8e0ece', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD0A4MY8DF6NB0KNV96RQADR', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Appearance settings category — theme picker control', 'The one custom control beyond the four field patterns: a theme-picker swatch grid with live mini-previews of the bundled themes (D-44 / D-69), selected state, and import-custom-YAML; plus high-contrast toggle and text-size. Coordinate with / extend the existing builtin.theme-picker (T-8) and per-repo theme persistence (T-293). Design pinned in T-302. Wireframe: docs/design/wireframes/settings/settings-appearance.png.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-16 11:19:41', '2026-06-17 10:56:30.288', NULL, 'e7d925e921ee3c791398c6b15ebc00f5', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD0A4MY8DF6NB0KNV96RQADR', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Appearance settings category — theme picker control', 'The one custom control beyond the four field patterns: a theme-picker swatch grid with live mini-previews of the bundled themes (D-44 / D-69), selected state, and import-custom-YAML; plus high-contrast toggle and text-size. Coordinate with / extend the existing builtin.theme-picker (T-8) and per-repo theme persistence (T-293). Design pinned in T-302. Wireframe: docs/design/wireframes/settings/settings-appearance.png.', 'done', 'medium', NULL, NULL, NULL, '2026-06-16 11:19:41', '2026-06-17 11:04:11.562', NULL, 'e3bf96df622cc32d69465ac3ec06d269', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15TY946689VZK3MNABSMDW', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Extensions settings category', 'List installed extensions and surface each extension''s registered settings (ext..* — the extension-owned config pattern from ui-design theme.md); per-extension enable/disable + configure. Reuses the field renderer; extensions register their schema the same way core subsystems do.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-16 13:20:41', '2026-06-17 11:21:19.581', NULL, '70ec508d70d32c037cbe69b24a280f52', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FD15TY946689VZK3MNABSMDW', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Extensions settings category', 'List installed extensions and surface each extension''s registered settings (ext..* — the extension-owned config pattern from ui-design theme.md); per-extension enable/disable + configure. Reuses the field renderer; extensions register their schema the same way core subsystems do.', 'done', 'medium', NULL, NULL, NULL, '2026-06-16 13:20:41', '2026-06-17 11:43:37.614', NULL, '09f569e8ad465363d95821413ff7d027', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/CHANGELOG.md b/CHANGELOG.md index afe14527..a5f4d716 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. escape hatch (`SettingsControlContribution` / `SettingsControlRegistry`) for one-off controls the generic field kinds can't express. (T-452) +- **Settings → Extensions tab.** A "watch this space" notice — installing and + toggling extensions arrives with third-party (Lua) support; built-ins stay + always-on for now. (T-456) + ### Changed - **Theme picker relabelled "Theme…".** The ⌘K theme picker's command title diff --git a/lib/builtin/extensions_ui/extensions_ui.dart b/lib/builtin/extensions_ui/extensions_ui.dart index b968b883..c8d831ba 100644 --- a/lib/builtin/extensions_ui/extensions_ui.dart +++ b/lib/builtin/extensions_ui/extensions_ui.dart @@ -1 +1,2 @@ export 'src/extension.dart'; +export 'src/extensions_notice.dart'; diff --git a/lib/builtin/extensions_ui/src/extension.dart b/lib/builtin/extensions_ui/src/extension.dart index a32803b2..1692b9fc 100644 --- a/lib/builtin/extensions_ui/src/extension.dart +++ b/lib/builtin/extensions_ui/src/extension.dart @@ -1,17 +1,39 @@ +import 'package:clide/builtin/extensions_ui/src/extensions_notice.dart'; import 'package:clide/extension/extension.dart'; +import 'package:clide/kernel/kernel.dart'; -/// Tier-0 stub. Real implementation lands in a later tier; the extension -/// is registered so the extensions-ui surface can list it as "installed, -/// not yet implemented" and its id is reserved. +/// Extensions settings tab (T-456). Built-in extensions are always on and there +/// is no third-party install path yet, so the tab is a "watch this space" +/// notice pointing at the records that track extension management (D-16 / T-8) +/// rather than a toggle list. The real enable/install UI lands with third-party +/// (Lua) extensions. class ExtensionsUiExtension extends ClideExtension { @override String get id => 'builtin.extensions-ui'; @override String get title => 'Extensions UI'; @override - String get version => '0.0.0-stub'; + String get version => '0.1.0'; @override List get dependsOn => const []; + @override - List get contributions => const []; + List get contributions => [ + SettingsControlContribution(id: 'extensions-ui.notice-control', customId: 'extensions.notice', builder: (_) => const ExtensionsNotice()), + const SettingsCategoryContribution( + id: 'extensions', + category: SettingsCategory( + id: 'extensions', + title: 'Extensions', + iconName: 'puzzle-piece', + priority: 80, + sections: [ + SettingsSection( + label: '', + fields: [SettingsField(key: 'app.extensions._notice', kind: SettingsFieldKind.custom, label: '', customId: 'extensions.notice')], + ), + ], + ), + ), + ]; } diff --git a/lib/builtin/extensions_ui/src/extensions_notice.dart b/lib/builtin/extensions_ui/src/extensions_notice.dart new file mode 100644 index 00000000..eb30d680 --- /dev/null +++ b/lib/builtin/extensions_ui/src/extensions_notice.dart @@ -0,0 +1,56 @@ +import 'package:clide/kernel/kernel.dart'; +import 'package:clide/widgets/widgets.dart'; +import 'package:flutter/widgets.dart'; + +/// The Extensions settings tab's "watch this space" notice (T-456). +/// +/// Built-in extensions are always on and there's no third-party install path +/// yet, so there's nothing to manage. Rather than ship a toggle list that could +/// brick the app, the tab explains that extension management arrives with +/// third-party (Lua) extensions and points at the records that track it. +/// Rendered inside the section card (no own surface) via the custom-control +/// registry. +class ExtensionsNotice extends StatelessWidget { + const ExtensionsNotice({super.key}); + + static const ns = 'builtin.extensions-ui'; + + @override + Widget build(BuildContext context) { + final tokens = ClideTheme.of(context).surface; + final i = ClideKernel.of(context).i18n; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + ClideIcon(PhosphorIcons.byName('puzzle-piece'), size: 18, color: tokens.globalTextMuted), + const SizedBox(width: 8), + ClideText( + i.string('notice.title', namespace: ns, placeholder: 'Extension management is coming'), + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ], + ), + const SizedBox(height: 10), + ClideText( + i.string( + 'notice.body', + namespace: ns, + placeholder: + 'Installing, enabling, and disabling extensions arrives with third-party (Lua) extension support. For now the built-in extensions are always on.', + ), + color: tokens.globalTextMuted, + ), + const SizedBox(height: 10), + ClideText( + i.string('notice.tracked', namespace: ns, placeholder: 'Tracked in T-8 (Tier 6) · D-16'), + color: tokens.globalTextMuted, + fontSize: clideFontCaption, + fontFamily: clideMonoFamily, + ), + ], + ); + } +} diff --git a/lib/builtin/settings_ui/src/settings_category_view.dart b/lib/builtin/settings_ui/src/settings_category_view.dart index 16889b59..577ce5df 100644 --- a/lib/builtin/settings_ui/src/settings_category_view.dart +++ b/lib/builtin/settings_ui/src/settings_category_view.dart @@ -125,10 +125,11 @@ class _SectionCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Padding( - padding: const EdgeInsets.only(left: 2, bottom: 6), - child: ClideText(section.label.toUpperCase(), fontSize: clideFontCaption, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), - ), + if (section.label.isNotEmpty) + Padding( + padding: const EdgeInsets.only(left: 2, bottom: 6), + child: ClideText(section.label.toUpperCase(), fontSize: clideFontCaption, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), + ), ClideSurface( // Card surface (surface.md): panelHeader resolves to the `surface` // palette key (the elevated card tone); inputs inside recede to @@ -181,8 +182,7 @@ class _FieldRow extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - label, - const SizedBox(height: 10), + if (field.label.isNotEmpty) ...[label, const SizedBox(height: 10)], builder?.call(context) ?? const SizedBox.shrink(), ], ), diff --git a/lib/kernel/src/i18n/catalog/builtin.extensions-ui_en_us.json b/lib/kernel/src/i18n/catalog/builtin.extensions-ui_en_us.json new file mode 100644 index 00000000..20e4aade --- /dev/null +++ b/lib/kernel/src/i18n/catalog/builtin.extensions-ui_en_us.json @@ -0,0 +1,5 @@ +{ + "notice.title": { "translation": "Extension management is coming" }, + "notice.body": { "translation": "Installing, enabling, and disabling extensions arrives with third-party (Lua) extension support. For now the built-in extensions are always on." }, + "notice.tracked": { "translation": "Tracked in T-8 (Tier 6) · D-16" } +} diff --git a/test/builtin/extensions_ui/extension_test.dart b/test/builtin/extensions_ui/extension_test.dart new file mode 100644 index 00000000..a0a66386 --- /dev/null +++ b/test/builtin/extensions_ui/extension_test.dart @@ -0,0 +1,31 @@ +import 'package:clide/builtin/extensions_ui/extensions_ui.dart'; +import 'package:clide/extension/extension.dart'; +import 'package:clide/kernel/kernel.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../helpers/kernel_fixture.dart'; +import '../../helpers/widget_harness.dart'; + +void main() { + group('ExtensionsUiExtension (T-456)', () { + late KernelFixture f; + setUp(() async => f = await KernelFixture.create()); + tearDown(() async => f.dispose()); + + test('contributes an Extensions category backed by the notice control', () { + final ext = ExtensionsUiExtension(); + final cat = ext.contributions.whereType().firstWhere((c) => c.id == 'extensions').category; + expect(cat.title, 'Extensions'); + final field = cat.sections.expand((s) => s.fields).single; + expect(field.kind, SettingsFieldKind.custom); + expect(field.customId, 'extensions.notice'); + expect(ext.contributions.whereType().any((c) => c.customId == 'extensions.notice'), isTrue); + }); + + testWidgets('the notice explains that management arrives later', (tester) async { + await tester.pumpWidget(harness(f, const SizedBox(width: 420, child: ExtensionsNotice()))); + expect(find.text('Extension management is coming'), findsOneWidget); + }); + }); +}