feat(studio): first-run UX, recovery affordances, l10n, bundled fonts

- Connection-aware Welcome: when the hub is down, show a hero with a
  primary "Start hub" CTA + install fallback instead of a dead,
  all-unchecked onboarding checklist (the first-run cliff).
- Actionable binary-not-found (file picker + install link, not a
  "set FAI_BIN" dead end) and a connect-failure banner after
  repeated failed health polls.
- Localize six hardcoded English error/toast clusters (DE+EN ARB).
- Bundle Inter + JetBrains Mono as assets; drop the runtime
  google_fonts fetch (air-gap / KRITIS safe, no font-swap flash).

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-06-11 23:52:18 +02:00
parent 7511867774
commit 5313266cc4
25 changed files with 1231 additions and 234 deletions

View file

@ -1520,5 +1520,79 @@
"placeholders": {
"error": {"type": "String"}
}
}
},
"welcomeHubDownTitle": "Der Hub läuft noch nicht",
"welcomeHubDownBody": "Studio spricht mit einem lokalen Hub — der Engine, die Module lädt und Flows ausführt. Sie ist gerade nicht erreichbar. Starte sie, um loszulegen.",
"welcomeHubDownStart": "Hub starten",
"welcomeHubDownStarting": "Wird gestartet …",
"welcomeHubDownDocsLink": "Klappt der Start nicht? Zur Erste-Schritte-Anleitung",
"welcomeHubDownEndpoint": "Endpunkt: {endpoint}",
"@welcomeHubDownEndpoint": {
"placeholders": {
"endpoint": {"type": "String"}
}
},
"daemonStartRequested": "Start des Daemons angefordert. Verbinde neu …",
"daemonStartFailed": "Daemon konnte nicht gestartet werden: {detail}",
"@daemonStartFailed": {
"placeholders": {
"detail": {"type": "String"}
}
},
"faiBinaryNotFound": "Das Programm `fai` wurde auf diesem Rechner nicht gefunden.",
"faiBinaryNotFoundHint": "Installiere die F∆I-Plattform oder verweise Studio auf eine vorhandene `fai`-Datei.",
"faiBinaryLocateButton": "`fai`-Programm suchen …",
"faiBinaryInstallDocsButton": "Installationsanleitung öffnen",
"faiBinaryPickerTitle": "`fai`-Programm auswählen",
"faiBinarySetOk": "Verwende `fai` unter {path}",
"@faiBinarySetOk": {
"placeholders": {
"path": {"type": "String"}
}
},
"sysAiFixParse": "Der Anbieter hat in einem unerwarteten Format geantwortet. Anbieter, die nicht OpenAI-kompatibel sind, brauchen ggf. einen Übersetzungs-Proxy.",
"sysAiFixUnknown": "Unerwarteter Fehler ({kind}). Siehe System-KI-Dokumentation.",
"@sysAiFixUnknown": {
"placeholders": {
"kind": {"type": "String"}
}
},
"sysAiFixDisabled": "Richte die System-KI in den Einstellungen (Cmd+,) → Bereich System-KI ein.",
"sysAiFixEnvMissing": "Die Umgebungsvariable für den API-Schlüssel ist leer. Setze sie in deiner Shell und starte den Daemon neu.",
"sysAiFixNetwork": "Der Hub erreicht den konfigurierten Endpunkt nicht. Läuft dein Anbieter?",
"sysAiFixHttp": "Der Anbieter hat einen Status außerhalb 2xx geliefert. Prüfe Endpunkt, Modellname und API-Schlüssel.",
"sysAiFixEmptyResponse": "Der Anbieter hat keinen Inhalt geliefert. Probiere ein anderes Modell oder formuliere den Prompt um.",
"channelSwitchOk": "Aktiver Kanal auf \"{name}\" umgeschaltet. Daemon neu gestartet.",
"@channelSwitchOk": {
"placeholders": {
"name": {"type": "String"}
}
},
"channelSwitchFailed": "Kanalwechsel fehlgeschlagen: {detail}",
"@channelSwitchFailed": {
"placeholders": {
"detail": {"type": "String"}
}
},
"providerDescOllama": "Lokales `ollama serve`. Die Modelle bleiben auf deinem Rechner. Kein API-Schlüssel nötig.",
"providerDescOpenai": "Gehostete OpenAI-API. Erfordert einen API-Schlüssel. Daten verlassen deinen Rechner.",
"providerDescLmstudio": "Lokaler LM-Studio-Server. Die Modelle bleiben auf deinem Rechner. Kein API-Schlüssel nötig.",
"providerDescVllm": "Selbst gehostetes vLLM oder ein anderer OpenAI-kompatibler Server. Endpunkt erforderlich.",
"providerDescCustom": "Alles andere, das die OpenAI-kompatible Chat-Completions-Schnittstelle spricht (LiteLLM-Proxy, interner Spiegel, …).",
"welcomeDocLoadFailedBody": "Die mitgelieferte Doku \"{slug}\" ({locale}) konnte nicht geladen werden.\nVersucht: {attempted}\nUrsache: {underlying}\nMeist ist der installierte Studio-Build älter als das Doku-Bundle. Baue Studio neu oder öffne die Online-Kopie.",
"@welcomeDocLoadFailedBody": {
"placeholders": {
"slug": {"type": "String"},
"locale": {"type": "String"},
"attempted": {"type": "String"},
"underlying": {"type": "String"}
}
},
"hubUnreachableBanner": "{endpoint} nicht erreichbar — Einstellungen öffnen",
"@hubUnreachableBanner": {
"placeholders": {
"endpoint": {"type": "String"}
}
},
"hubUnreachableOpenSettings": "Einstellungen öffnen"
}

View file

@ -1523,5 +1523,79 @@
"placeholders": {
"error": {"type": "String"}
}
}
},
"welcomeHubDownTitle": "The hub isn't running yet",
"welcomeHubDownBody": "Studio talks to a local hub — the engine that loads modules and runs flows. It isn't reachable right now. Start it to begin.",
"welcomeHubDownStart": "Start hub",
"welcomeHubDownStarting": "Starting…",
"welcomeHubDownDocsLink": "Can't start it? Read the getting-started guide",
"welcomeHubDownEndpoint": "Endpoint: {endpoint}",
"@welcomeHubDownEndpoint": {
"placeholders": {
"endpoint": {"type": "String"}
}
},
"daemonStartRequested": "Daemon start requested. Reconnecting…",
"daemonStartFailed": "Could not start daemon: {detail}",
"@daemonStartFailed": {
"placeholders": {
"detail": {"type": "String"}
}
},
"faiBinaryNotFound": "Could not find the `fai` program on this machine.",
"faiBinaryNotFoundHint": "Install the F∆I platform, or point Studio at an existing `fai` binary.",
"faiBinaryLocateButton": "Locate `fai` binary…",
"faiBinaryInstallDocsButton": "Open install guide",
"faiBinaryPickerTitle": "Select the `fai` binary",
"faiBinarySetOk": "Using `fai` binary at {path}",
"@faiBinarySetOk": {
"placeholders": {
"path": {"type": "String"}
}
},
"sysAiFixParse": "The provider sent a response in an unexpected format. Providers that aren't OpenAI-compatible may need a translation proxy.",
"sysAiFixUnknown": "Unexpected failure ({kind}). See the System AI documentation.",
"@sysAiFixUnknown": {
"placeholders": {
"kind": {"type": "String"}
}
},
"sysAiFixDisabled": "Configure System AI in Settings (Cmd+,) → System AI panel.",
"sysAiFixEnvMissing": "The API-key environment variable is empty. Set it in your shell, then restart the daemon.",
"sysAiFixNetwork": "The hub can't reach the configured endpoint. Is your provider running?",
"sysAiFixHttp": "The provider returned a non-2xx status. Verify the endpoint, model name, and API key.",
"sysAiFixEmptyResponse": "The provider answered with no content. Try a different model or rephrase the prompt.",
"channelSwitchOk": "Switched active channel to \"{name}\". Daemon restarted.",
"@channelSwitchOk": {
"placeholders": {
"name": {"type": "String"}
}
},
"channelSwitchFailed": "Channel switch failed: {detail}",
"@channelSwitchFailed": {
"placeholders": {
"detail": {"type": "String"}
}
},
"providerDescOllama": "Local `ollama serve`. Models stay on your machine. No API key needed.",
"providerDescOpenai": "OpenAI hosted API. Requires an API key. Data leaves your machine.",
"providerDescLmstudio": "Local LM Studio server. Models stay on your machine. No API key needed.",
"providerDescVllm": "Self-hosted vLLM or any other OpenAI-compatible server. Endpoint required.",
"providerDescCustom": "Anything else that speaks the OpenAI-compatible chat-completions wire (LiteLLM proxy, internal mirror, …).",
"welcomeDocLoadFailedBody": "Could not load the bundled doc \"{slug}\" ({locale}).\nTried: {attempted}\nUnderlying: {underlying}\nThis usually means the Studio build on disk predates the doc bundle. Rebuild Studio or open the online copy.",
"@welcomeDocLoadFailedBody": {
"placeholders": {
"slug": {"type": "String"},
"locale": {"type": "String"},
"attempted": {"type": "String"},
"underlying": {"type": "String"}
}
},
"hubUnreachableBanner": "Can't reach {endpoint} — open Settings",
"@hubUnreachableBanner": {
"placeholders": {
"endpoint": {"type": "String"}
}
},
"hubUnreachableOpenSettings": "Open Settings"
}

View file

@ -4279,6 +4279,197 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Install failed: {error}'**
String installFailed(String error);
/// No description provided for @welcomeHubDownTitle.
///
/// In en, this message translates to:
/// **'The hub isn\'t running yet'**
String get welcomeHubDownTitle;
/// No description provided for @welcomeHubDownBody.
///
/// In en, this message translates to:
/// **'Studio talks to a local hub — the engine that loads modules and runs flows. It isn\'t reachable right now. Start it to begin.'**
String get welcomeHubDownBody;
/// No description provided for @welcomeHubDownStart.
///
/// In en, this message translates to:
/// **'Start hub'**
String get welcomeHubDownStart;
/// No description provided for @welcomeHubDownStarting.
///
/// In en, this message translates to:
/// **'Starting…'**
String get welcomeHubDownStarting;
/// No description provided for @welcomeHubDownDocsLink.
///
/// In en, this message translates to:
/// **'Can\'t start it? Read the getting-started guide'**
String get welcomeHubDownDocsLink;
/// No description provided for @welcomeHubDownEndpoint.
///
/// In en, this message translates to:
/// **'Endpoint: {endpoint}'**
String welcomeHubDownEndpoint(String endpoint);
/// No description provided for @daemonStartRequested.
///
/// In en, this message translates to:
/// **'Daemon start requested. Reconnecting…'**
String get daemonStartRequested;
/// No description provided for @daemonStartFailed.
///
/// In en, this message translates to:
/// **'Could not start daemon: {detail}'**
String daemonStartFailed(String detail);
/// No description provided for @faiBinaryNotFound.
///
/// In en, this message translates to:
/// **'Could not find the `fai` program on this machine.'**
String get faiBinaryNotFound;
/// No description provided for @faiBinaryNotFoundHint.
///
/// In en, this message translates to:
/// **'Install the F∆I platform, or point Studio at an existing `fai` binary.'**
String get faiBinaryNotFoundHint;
/// No description provided for @faiBinaryLocateButton.
///
/// In en, this message translates to:
/// **'Locate `fai` binary…'**
String get faiBinaryLocateButton;
/// No description provided for @faiBinaryInstallDocsButton.
///
/// In en, this message translates to:
/// **'Open install guide'**
String get faiBinaryInstallDocsButton;
/// No description provided for @faiBinaryPickerTitle.
///
/// In en, this message translates to:
/// **'Select the `fai` binary'**
String get faiBinaryPickerTitle;
/// No description provided for @faiBinarySetOk.
///
/// In en, this message translates to:
/// **'Using `fai` binary at {path}'**
String faiBinarySetOk(String path);
/// No description provided for @sysAiFixParse.
///
/// In en, this message translates to:
/// **'The provider sent a response in an unexpected format. Providers that aren\'t OpenAI-compatible may need a translation proxy.'**
String get sysAiFixParse;
/// No description provided for @sysAiFixUnknown.
///
/// In en, this message translates to:
/// **'Unexpected failure ({kind}). See the System AI documentation.'**
String sysAiFixUnknown(String kind);
/// No description provided for @sysAiFixDisabled.
///
/// In en, this message translates to:
/// **'Configure System AI in Settings (Cmd+,) → System AI panel.'**
String get sysAiFixDisabled;
/// No description provided for @sysAiFixEnvMissing.
///
/// In en, this message translates to:
/// **'The API-key environment variable is empty. Set it in your shell, then restart the daemon.'**
String get sysAiFixEnvMissing;
/// No description provided for @sysAiFixNetwork.
///
/// In en, this message translates to:
/// **'The hub can\'t reach the configured endpoint. Is your provider running?'**
String get sysAiFixNetwork;
/// No description provided for @sysAiFixHttp.
///
/// In en, this message translates to:
/// **'The provider returned a non-2xx status. Verify the endpoint, model name, and API key.'**
String get sysAiFixHttp;
/// No description provided for @sysAiFixEmptyResponse.
///
/// In en, this message translates to:
/// **'The provider answered with no content. Try a different model or rephrase the prompt.'**
String get sysAiFixEmptyResponse;
/// No description provided for @channelSwitchOk.
///
/// In en, this message translates to:
/// **'Switched active channel to \"{name}\". Daemon restarted.'**
String channelSwitchOk(String name);
/// No description provided for @channelSwitchFailed.
///
/// In en, this message translates to:
/// **'Channel switch failed: {detail}'**
String channelSwitchFailed(String detail);
/// No description provided for @providerDescOllama.
///
/// In en, this message translates to:
/// **'Local `ollama serve`. Models stay on your machine. No API key needed.'**
String get providerDescOllama;
/// No description provided for @providerDescOpenai.
///
/// In en, this message translates to:
/// **'OpenAI hosted API. Requires an API key. Data leaves your machine.'**
String get providerDescOpenai;
/// No description provided for @providerDescLmstudio.
///
/// In en, this message translates to:
/// **'Local LM Studio server. Models stay on your machine. No API key needed.'**
String get providerDescLmstudio;
/// No description provided for @providerDescVllm.
///
/// In en, this message translates to:
/// **'Self-hosted vLLM or any other OpenAI-compatible server. Endpoint required.'**
String get providerDescVllm;
/// No description provided for @providerDescCustom.
///
/// In en, this message translates to:
/// **'Anything else that speaks the OpenAI-compatible chat-completions wire (LiteLLM proxy, internal mirror, …).'**
String get providerDescCustom;
/// No description provided for @welcomeDocLoadFailedBody.
///
/// In en, this message translates to:
/// **'Could not load the bundled doc \"{slug}\" ({locale}).\nTried: {attempted}\nUnderlying: {underlying}\nThis usually means the Studio build on disk predates the doc bundle. Rebuild Studio or open the online copy.'**
String welcomeDocLoadFailedBody(
String slug,
String locale,
String attempted,
String underlying,
);
/// No description provided for @hubUnreachableBanner.
///
/// In en, this message translates to:
/// **'Can\'t reach {endpoint} — open Settings'**
String hubUnreachableBanner(String endpoint);
/// No description provided for @hubUnreachableOpenSettings.
///
/// In en, this message translates to:
/// **'Open Settings'**
String get hubUnreachableOpenSettings;
}
class _AppLocalizationsDelegate

View file

@ -2502,4 +2502,134 @@ class AppLocalizationsDe extends AppLocalizations {
String installFailed(String error) {
return 'Installation fehlgeschlagen: $error';
}
@override
String get welcomeHubDownTitle => 'Der Hub läuft noch nicht';
@override
String get welcomeHubDownBody =>
'Studio spricht mit einem lokalen Hub — der Engine, die Module lädt und Flows ausführt. Sie ist gerade nicht erreichbar. Starte sie, um loszulegen.';
@override
String get welcomeHubDownStart => 'Hub starten';
@override
String get welcomeHubDownStarting => 'Wird gestartet …';
@override
String get welcomeHubDownDocsLink =>
'Klappt der Start nicht? Zur Erste-Schritte-Anleitung';
@override
String welcomeHubDownEndpoint(String endpoint) {
return 'Endpunkt: $endpoint';
}
@override
String get daemonStartRequested =>
'Start des Daemons angefordert. Verbinde neu …';
@override
String daemonStartFailed(String detail) {
return 'Daemon konnte nicht gestartet werden: $detail';
}
@override
String get faiBinaryNotFound =>
'Das Programm `fai` wurde auf diesem Rechner nicht gefunden.';
@override
String get faiBinaryNotFoundHint =>
'Installiere die F∆I-Plattform oder verweise Studio auf eine vorhandene `fai`-Datei.';
@override
String get faiBinaryLocateButton => '`fai`-Programm suchen …';
@override
String get faiBinaryInstallDocsButton => 'Installationsanleitung öffnen';
@override
String get faiBinaryPickerTitle => '`fai`-Programm auswählen';
@override
String faiBinarySetOk(String path) {
return 'Verwende `fai` unter $path';
}
@override
String get sysAiFixParse =>
'Der Anbieter hat in einem unerwarteten Format geantwortet. Anbieter, die nicht OpenAI-kompatibel sind, brauchen ggf. einen Übersetzungs-Proxy.';
@override
String sysAiFixUnknown(String kind) {
return 'Unerwarteter Fehler ($kind). Siehe System-KI-Dokumentation.';
}
@override
String get sysAiFixDisabled =>
'Richte die System-KI in den Einstellungen (Cmd+,) → Bereich System-KI ein.';
@override
String get sysAiFixEnvMissing =>
'Die Umgebungsvariable für den API-Schlüssel ist leer. Setze sie in deiner Shell und starte den Daemon neu.';
@override
String get sysAiFixNetwork =>
'Der Hub erreicht den konfigurierten Endpunkt nicht. Läuft dein Anbieter?';
@override
String get sysAiFixHttp =>
'Der Anbieter hat einen Status außerhalb 2xx geliefert. Prüfe Endpunkt, Modellname und API-Schlüssel.';
@override
String get sysAiFixEmptyResponse =>
'Der Anbieter hat keinen Inhalt geliefert. Probiere ein anderes Modell oder formuliere den Prompt um.';
@override
String channelSwitchOk(String name) {
return 'Aktiver Kanal auf \"$name\" umgeschaltet. Daemon neu gestartet.';
}
@override
String channelSwitchFailed(String detail) {
return 'Kanalwechsel fehlgeschlagen: $detail';
}
@override
String get providerDescOllama =>
'Lokales `ollama serve`. Die Modelle bleiben auf deinem Rechner. Kein API-Schlüssel nötig.';
@override
String get providerDescOpenai =>
'Gehostete OpenAI-API. Erfordert einen API-Schlüssel. Daten verlassen deinen Rechner.';
@override
String get providerDescLmstudio =>
'Lokaler LM-Studio-Server. Die Modelle bleiben auf deinem Rechner. Kein API-Schlüssel nötig.';
@override
String get providerDescVllm =>
'Selbst gehostetes vLLM oder ein anderer OpenAI-kompatibler Server. Endpunkt erforderlich.';
@override
String get providerDescCustom =>
'Alles andere, das die OpenAI-kompatible Chat-Completions-Schnittstelle spricht (LiteLLM-Proxy, interner Spiegel, …).';
@override
String welcomeDocLoadFailedBody(
String slug,
String locale,
String attempted,
String underlying,
) {
return 'Die mitgelieferte Doku \"$slug\" ($locale) konnte nicht geladen werden.\nVersucht: $attempted\nUrsache: $underlying\nMeist ist der installierte Studio-Build älter als das Doku-Bundle. Baue Studio neu oder öffne die Online-Kopie.';
}
@override
String hubUnreachableBanner(String endpoint) {
return '$endpoint nicht erreichbar — Einstellungen öffnen';
}
@override
String get hubUnreachableOpenSettings => 'Einstellungen öffnen';
}

View file

@ -2506,4 +2506,133 @@ class AppLocalizationsEn extends AppLocalizations {
String installFailed(String error) {
return 'Install failed: $error';
}
@override
String get welcomeHubDownTitle => 'The hub isn\'t running yet';
@override
String get welcomeHubDownBody =>
'Studio talks to a local hub — the engine that loads modules and runs flows. It isn\'t reachable right now. Start it to begin.';
@override
String get welcomeHubDownStart => 'Start hub';
@override
String get welcomeHubDownStarting => 'Starting…';
@override
String get welcomeHubDownDocsLink =>
'Can\'t start it? Read the getting-started guide';
@override
String welcomeHubDownEndpoint(String endpoint) {
return 'Endpoint: $endpoint';
}
@override
String get daemonStartRequested => 'Daemon start requested. Reconnecting…';
@override
String daemonStartFailed(String detail) {
return 'Could not start daemon: $detail';
}
@override
String get faiBinaryNotFound =>
'Could not find the `fai` program on this machine.';
@override
String get faiBinaryNotFoundHint =>
'Install the F∆I platform, or point Studio at an existing `fai` binary.';
@override
String get faiBinaryLocateButton => 'Locate `fai` binary…';
@override
String get faiBinaryInstallDocsButton => 'Open install guide';
@override
String get faiBinaryPickerTitle => 'Select the `fai` binary';
@override
String faiBinarySetOk(String path) {
return 'Using `fai` binary at $path';
}
@override
String get sysAiFixParse =>
'The provider sent a response in an unexpected format. Providers that aren\'t OpenAI-compatible may need a translation proxy.';
@override
String sysAiFixUnknown(String kind) {
return 'Unexpected failure ($kind). See the System AI documentation.';
}
@override
String get sysAiFixDisabled =>
'Configure System AI in Settings (Cmd+,) → System AI panel.';
@override
String get sysAiFixEnvMissing =>
'The API-key environment variable is empty. Set it in your shell, then restart the daemon.';
@override
String get sysAiFixNetwork =>
'The hub can\'t reach the configured endpoint. Is your provider running?';
@override
String get sysAiFixHttp =>
'The provider returned a non-2xx status. Verify the endpoint, model name, and API key.';
@override
String get sysAiFixEmptyResponse =>
'The provider answered with no content. Try a different model or rephrase the prompt.';
@override
String channelSwitchOk(String name) {
return 'Switched active channel to \"$name\". Daemon restarted.';
}
@override
String channelSwitchFailed(String detail) {
return 'Channel switch failed: $detail';
}
@override
String get providerDescOllama =>
'Local `ollama serve`. Models stay on your machine. No API key needed.';
@override
String get providerDescOpenai =>
'OpenAI hosted API. Requires an API key. Data leaves your machine.';
@override
String get providerDescLmstudio =>
'Local LM Studio server. Models stay on your machine. No API key needed.';
@override
String get providerDescVllm =>
'Self-hosted vLLM or any other OpenAI-compatible server. Endpoint required.';
@override
String get providerDescCustom =>
'Anything else that speaks the OpenAI-compatible chat-completions wire (LiteLLM proxy, internal mirror, …).';
@override
String welcomeDocLoadFailedBody(
String slug,
String locale,
String attempted,
String underlying,
) {
return 'Could not load the bundled doc \"$slug\" ($locale).\nTried: $attempted\nUnderlying: $underlying\nThis usually means the Studio build on disk predates the doc bundle. Rebuild Studio or open the online copy.';
}
@override
String hubUnreachableBanner(String endpoint) {
return 'Can\'t reach $endpoint — open Settings';
}
@override
String get hubUnreachableOpenSettings => 'Open Settings';
}