feat(settings): Extensions tab — "watch this space" notice (T-456)

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 13:45:01 +02:00
co-authored by Claude Opus 4.8
parent 757d6f71fa
commit 79c5294d44
9 changed files with 134 additions and 11 deletions
@@ -1 +1,2 @@
export 'src/extension.dart';
export 'src/extensions_notice.dart';
+27 -5
View File
@@ -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<String> get dependsOn => const [];
@override
List<ContributionPoint> get contributions => const [];
List<ContributionPoint> 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')],
),
],
),
),
];
}
@@ -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,
),
],
);
}
}
@@ -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(),
],
),
@@ -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" }
}