feat(settings): per-field scope tags + scope resolution (T-449)

Each settings field gains a scope tag showing where its value lives — folder
= Project (.clide), globe = Always (~/.clide), circle-dashed = Default/unset —
colour-coded (statusSuccess / statusWarning / muted) with a tooltip. Tapping
opens a menu to move the value between the scopes the key supports, or reset
to default; the tag's menu replaces the interim reset button.

Backs it with scope-explicit SettingsStore access — rawAt / setAt / removeAt /
effectiveLayer / writableLayers — over the two storage files (app ~/.clide,
project .clide). ext.* keys layer project-over-app; app.*/project.* keys live
only in their prefix's file, so their menu offers that one scope + reset.

Tests: store scope ops (layering, reload, guards) and the tag (Default vs
All-clide rendering, menu reset).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 12:18:38 +02:00
co-authored by Claude Opus 4.8
parent 6c6b0c731e
commit f56ad88439
11 changed files with 320 additions and 51 deletions
@@ -102,14 +102,28 @@ void main() {
expect(f.services.settings.get<String>('app.demo.level'), 'debug');
});
testWidgets('reset control appears for a non-default value and restores the default', (tester) async {
});
group('scope tag (T-449)', () {
testWidgets('an unset field shows the Default scope tag', (tester) async {
await tester.pumpWidget(harness(f, _bounded(const SettingsCategoryView(category: _category))));
expect(find.bySemanticsLabel('Size scope: Default'), findsOneWidget);
});
testWidgets('a value stored at app scope shows the All clide tag', (tester) async {
await tester.runAsync(() => f.services.settings.set('app.demo.flag', true));
await tester.pumpWidget(harness(f, _bounded(const SettingsCategoryView(category: _category))));
final reset = find.bySemanticsLabel('Reset to default');
expect(reset, findsOneWidget);
await tester.tap(reset);
expect(find.bySemanticsLabel('Flag scope: All clide'), findsOneWidget);
});
testWidgets('the scope menu resets the value to default', (tester) async {
await tester.runAsync(() => f.services.settings.set('app.demo.flag', true));
await tester.pumpWidget(harness(f, _bounded(const SettingsCategoryView(category: _category))));
await tester.tap(find.bySemanticsLabel('Flag scope: All clide'));
await tester.pump();
expect(f.services.settings.get<bool>('app.demo.flag'), isFalse);
await tester.tap(find.text('Reset to default'));
await tester.pump();
expect(f.services.settings.effectiveLayer('app.demo.flag'), isNull);
});
});