fix(studio): theme picker confirms applied + surfaces load failures
Some checks failed
Security / Security check (push) Failing after 2s

Two operator-visible fixes prompted by 'I picked a theme but
nothing changed':

  - Theme picker tile taps now show a brief "Theme applied:
    <name>" snackbar — the picker applies themes instantly, so
    the operator needs a confirming cue. Slow `_pluginThemes`
    loads no longer read as "click did nothing".
  - `_pluginThemes` failures (plugin unreachable, manifest
    drift, etc.) write to `FaiLog` so `fai admin doctor` and
    Studio's inline log viewer can surface them. Previously
    swallowed silently, which is what made debugging this so
    miserable.
  - Settings dialog's primary button relabelled to
    "Connect to endpoint" (was "Save & connect") so operators
    don't mistake it for "save my theme choice". The theme
    selection persists at tile-tap time; the Settings dialog
    has nothing left to "save".
  - Theme section header gains an explicit "(applies instantly)"
    cue for the same reason.

Also fixes a pre-existing curly-braces lint in the n8n add-
endpoint dialog. Studio bumped to 0.62.1, editor pinned via
path override at 0.15.1.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-06-04 02:25:11 +02:00
parent 8f5cd2528b
commit 805f1b4c1f
9 changed files with 91 additions and 17 deletions

View file

@ -104,7 +104,14 @@
"mcpSuggestionMemoryDesc": "Anthropic — persistenter Knowledge-Graph als Agenten-Gedächtnis.", "mcpSuggestionMemoryDesc": "Anthropic — persistenter Knowledge-Graph als Agenten-Gedächtnis.",
"mcpSuggestionTimeDesc": "Anthropic — aktuelle Uhrzeit + Zeitzonen-Konvertierung.", "mcpSuggestionTimeDesc": "Anthropic — aktuelle Uhrzeit + Zeitzonen-Konvertierung.",
"mcpServerEnLanguageBadge": "Server-Text in Englisch — Studio zeigt ihn wie geliefert.", "mcpServerEnLanguageBadge": "Server-Text in Englisch — Studio zeigt ihn wie geliefert.",
"themePluginHeader": "Theme-Plugin", "themePluginHeader": "Theme-Plugin (wirkt sofort)",
"themeApplied": "Theme aktiv: {name}",
"@themeApplied": {
"placeholders": {
"name": {"type": "String"}
}
},
"themePluginFailed": "Theme-Plugin konnte nicht geladen werden — vorheriges Theme bleibt.",
"themePluginNone": "Standard (kein Plugin)", "themePluginNone": "Standard (kein Plugin)",
"themePluginHint": "Built-in- und Plugin-Kacheln bringen handkuratierte Paletten mit. Die Kachel „Eigene Farbe\" leitet aus einer gewählten Seedfarbe eine vollständige Material-3-Palette ab (primary, secondary, tertiary, surface, …) — das ist ein vollwertiges Theme, kein Einzelfarb-Tweak.", "themePluginHint": "Built-in- und Plugin-Kacheln bringen handkuratierte Paletten mit. Die Kachel „Eigene Farbe\" leitet aus einer gewählten Seedfarbe eine vollständige Material-3-Palette ab (primary, secondary, tertiary, surface, …) — das ist ein vollwertiges Theme, kein Einzelfarb-Tweak.",
"themePluginEmpty": "Kein Theme-Plugin installiert. Im Store eines aus der Kategorie studio-plugin auswählen.", "themePluginEmpty": "Kein Theme-Plugin installiert. Im Store eines aus der Kategorie studio-plugin auswählen.",
@ -118,7 +125,7 @@
"settingsHost": "Host", "settingsHost": "Host",
"settingsPort": "Port", "settingsPort": "Port",
"settingsTls": "TLS", "settingsTls": "TLS",
"settingsSaveAndConnect": "Speichern & verbinden", "settingsSaveAndConnect": "Mit Endpoint verbinden",
"settingsLanguage": "Sprache", "settingsLanguage": "Sprache",
"settingsLanguageHint": "Sprache der App. Wirkt auf alle Seiten; mehrsprachige Store-Einträge folgen ebenfalls.", "settingsLanguageHint": "Sprache der App. Wirkt auf alle Seiten; mehrsprachige Store-Einträge folgen ebenfalls.",
"settingsHubEndpointHint": "Wohin soll Studio sich verbinden? Standard ist der lokale Hub auf 127.0.0.1:50051.", "settingsHubEndpointHint": "Wohin soll Studio sich verbinden? Standard ist der lokale Hub auf 127.0.0.1:50051.",

View file

@ -107,7 +107,14 @@
"mcpSuggestionMemoryDesc": "Anthropic — persistent knowledge graph for an agent's memory.", "mcpSuggestionMemoryDesc": "Anthropic — persistent knowledge graph for an agent's memory.",
"mcpSuggestionTimeDesc": "Anthropic — current time + timezone conversion.", "mcpSuggestionTimeDesc": "Anthropic — current time + timezone conversion.",
"mcpServerEnLanguageBadge": "Server text in English — Studio shows it as supplied.", "mcpServerEnLanguageBadge": "Server text in English — Studio shows it as supplied.",
"themePluginHeader": "Theme plugin", "themePluginHeader": "Theme plugin (applies instantly)",
"themeApplied": "Theme applied: {name}",
"@themeApplied": {
"placeholders": {
"name": {"type": "String"}
}
},
"themePluginFailed": "Theme plugin could not load — keeping previous theme.",
"themePluginNone": "Built-in (no plugin)", "themePluginNone": "Built-in (no plugin)",
"themePluginHint": "Built-in and plugin tiles ship hand-curated palettes. The Custom tile derives a full Material 3 palette (primary, secondary, tertiary, surface, …) from one seed colour you pick — it's a complete theme, not a single-colour tweak.", "themePluginHint": "Built-in and plugin tiles ship hand-curated palettes. The Custom tile derives a full Material 3 palette (primary, secondary, tertiary, surface, …) from one seed colour you pick — it's a complete theme, not a single-colour tweak.",
"themePluginEmpty": "No theme plugins installed. Open the Store and pick one in the studio-plugin category.", "themePluginEmpty": "No theme plugins installed. Open the Store and pick one in the studio-plugin category.",
@ -121,7 +128,7 @@
"settingsHost": "Host", "settingsHost": "Host",
"settingsPort": "Port", "settingsPort": "Port",
"settingsTls": "TLS", "settingsTls": "TLS",
"settingsSaveAndConnect": "Save & connect", "settingsSaveAndConnect": "Connect to endpoint",
"settingsLanguage": "Language", "settingsLanguage": "Language",
"settingsLanguageHint": "App language. Affects every page; bilingual store entries also flip via this toggle.", "settingsLanguageHint": "App language. Affects every page; bilingual store entries also flip via this toggle.",
"settingsHubEndpointHint": "Where should Studio connect? Default is the local hub at 127.0.0.1:50051.", "settingsHubEndpointHint": "Where should Studio connect? Default is the local hub at 127.0.0.1:50051.",

View file

@ -641,9 +641,21 @@ abstract class AppLocalizations {
/// No description provided for @themePluginHeader. /// No description provided for @themePluginHeader.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Theme plugin'** /// **'Theme plugin (applies instantly)'**
String get themePluginHeader; String get themePluginHeader;
/// No description provided for @themeApplied.
///
/// In en, this message translates to:
/// **'Theme applied: {name}'**
String themeApplied(String name);
/// No description provided for @themePluginFailed.
///
/// In en, this message translates to:
/// **'Theme plugin could not load — keeping previous theme.'**
String get themePluginFailed;
/// No description provided for @themePluginNone. /// No description provided for @themePluginNone.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -725,7 +737,7 @@ abstract class AppLocalizations {
/// No description provided for @settingsSaveAndConnect. /// No description provided for @settingsSaveAndConnect.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Save & connect'** /// **'Connect to endpoint'**
String get settingsSaveAndConnect; String get settingsSaveAndConnect;
/// No description provided for @settingsLanguage. /// No description provided for @settingsLanguage.

View file

@ -313,7 +313,16 @@ class AppLocalizationsDe extends AppLocalizations {
'Server-Text in Englisch — Studio zeigt ihn wie geliefert.'; 'Server-Text in Englisch — Studio zeigt ihn wie geliefert.';
@override @override
String get themePluginHeader => 'Theme-Plugin'; String get themePluginHeader => 'Theme-Plugin (wirkt sofort)';
@override
String themeApplied(String name) {
return 'Theme aktiv: $name';
}
@override
String get themePluginFailed =>
'Theme-Plugin konnte nicht geladen werden — vorheriges Theme bleibt.';
@override @override
String get themePluginNone => 'Standard (kein Plugin)'; String get themePluginNone => 'Standard (kein Plugin)';
@ -357,7 +366,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get settingsTls => 'TLS'; String get settingsTls => 'TLS';
@override @override
String get settingsSaveAndConnect => 'Speichern & verbinden'; String get settingsSaveAndConnect => 'Mit Endpoint verbinden';
@override @override
String get settingsLanguage => 'Sprache'; String get settingsLanguage => 'Sprache';

View file

@ -313,7 +313,16 @@ class AppLocalizationsEn extends AppLocalizations {
'Server text in English — Studio shows it as supplied.'; 'Server text in English — Studio shows it as supplied.';
@override @override
String get themePluginHeader => 'Theme plugin'; String get themePluginHeader => 'Theme plugin (applies instantly)';
@override
String themeApplied(String name) {
return 'Theme applied: $name';
}
@override
String get themePluginFailed =>
'Theme plugin could not load — keeping previous theme.';
@override @override
String get themePluginNone => 'Built-in (no plugin)'; String get themePluginNone => 'Built-in (no plugin)';
@ -358,7 +367,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get settingsTls => 'TLS'; String get settingsTls => 'TLS';
@override @override
String get settingsSaveAndConnect => 'Save & connect'; String get settingsSaveAndConnect => 'Connect to endpoint';
@override @override
String get settingsLanguage => 'Language'; String get settingsLanguage => 'Language';

View file

@ -8,6 +8,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'data/fai_log.dart';
import 'data/hub.dart'; import 'data/hub.dart';
import 'data/system_actions.dart'; import 'data/system_actions.dart';
import 'data/theme_plugin.dart'; import 'data/theme_plugin.dart';
@ -26,7 +27,7 @@ import 'widgets/widgets.dart';
/// Studio's own build version. Bump on every UI commit so the /// Studio's own build version. Bump on every UI commit so the
/// running app self-identifies visible in the sidebar header /// running app self-identifies visible in the sidebar header
/// and quick-glance proof that you're seeing the current build. /// and quick-glance proof that you're seeing the current build.
const String kStudioVersion = '0.62.0'; const String kStudioVersion = '0.62.1';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -178,10 +179,18 @@ class StudioAppState extends State<StudioApp> {
: themeDataFromScheme(schemes.light!), : themeDataFromScheme(schemes.light!),
dark: schemes.dark == null ? null : themeDataFromScheme(schemes.dark!), dark: schemes.dark == null ? null : themeDataFromScheme(schemes.dark!),
); );
} catch (_) { } catch (e) {
// Plugin unreachable, hub down, etc. Fall back to // Plugin unreachable, hub down, manifest schema drift
// FaiTheme defaults silently the Settings dialog is // any of these end up here. Log the failure so doctor +
// where the operator surfaces / fixes the problem. // the in-app log viewer can surface it; the picker's
// own "applied" snackbar is replaced upstream by the
// friendlier failure message via [themePluginFailed].
// ignore: discarded_futures
FaiLog.instance.error(
'theme.plugin.load',
e,
context: 'capability=$capability',
);
return (light: null, dark: null); return (light: null, dark: null);
} }
} }

View file

@ -1472,8 +1472,9 @@ class _AddN8nEndpointDialogState extends State<_AddN8nEndpointDialog> {
), ),
FilledButton( FilledButton(
onPressed: () { onPressed: () {
if (_name.text.trim().isEmpty || _baseUrl.text.trim().isEmpty) if (_name.text.trim().isEmpty || _baseUrl.text.trim().isEmpty) {
return; return;
}
Navigator.pop( Navigator.pop(
context, context,
N8nEndpointDraft( N8nEndpointDraft(

View file

@ -109,11 +109,29 @@ class _ThemePickerGridState extends State<ThemePickerGrid> {
selected: active == null, selected: active == null,
onTap: () { onTap: () {
StudioApp.of(context)?.setThemePlugin(null); StudioApp.of(context)?.setThemePlugin(null);
_confirmApplied(l, l.themePluginNone);
setState(() {}); setState(() {});
}, },
); );
} }
/// Brief snackbar acknowledging the theme switch. The picker
/// applies themes instantly (no Save), so the operator needs
/// a visible cue that the click was registered without it,
/// a slow theme load looks like "the click did nothing".
void _confirmApplied(AppLocalizations l, String displayName) {
final messenger = ScaffoldMessenger.maybeOf(context);
if (messenger == null) return;
messenger.removeCurrentSnackBar();
messenger.showSnackBar(
SnackBar(
content: Text(l.themeApplied(displayName)),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
),
);
}
Widget _pluginTile(ThemeData theme, String cap, String? active) { Widget _pluginTile(ThemeData theme, String cap, String? active) {
// Pretty name: studio.theme.sunflower "Sunflower". // Pretty name: studio.theme.sunflower "Sunflower".
final display = _displayName(cap); final display = _displayName(cap);
@ -138,6 +156,7 @@ class _ThemePickerGridState extends State<ThemePickerGrid> {
loading: preview == null, loading: preview == null,
onTap: () { onTap: () {
StudioApp.of(context)?.setThemePlugin(cap); StudioApp.of(context)?.setThemePlugin(cap);
_confirmApplied(AppLocalizations.of(context)!, display);
setState(() {}); setState(() {});
}, },
); );
@ -176,6 +195,7 @@ class _ThemePickerGridState extends State<ThemePickerGrid> {
// Strip alpha component we only persist the RGB. // Strip alpha component we only persist the RGB.
final rgb = hex.substring(2).toUpperCase(); final rgb = hex.substring(2).toUpperCase();
StudioApp.of(context)?.setThemePlugin('custom:#$rgb'); StudioApp.of(context)?.setThemePlugin('custom:#$rgb');
_confirmApplied(l, '#$rgb');
setState(() {}); setState(() {});
}, },
); );

View file

@ -1,7 +1,7 @@
name: fai_studio name: fai_studio
description: "F∆I Studio — desktop GUI for the F∆I hub" description: "F∆I Studio — desktop GUI for the F∆I hub"
publish_to: 'none' publish_to: 'none'
version: 0.62.0 version: 0.62.1
environment: environment:
sdk: ^3.11.0-200.1.beta sdk: ^3.11.0-200.1.beta