feat(studio): system-ai editor — test-without-save + model picker (v0.13.0)

Three operator-facing improvements driven by Stefan's testing:

  * **Test Connection** runs against the form's *current* values
    and no longer requires saving first. Previously we save-then-
    test which caused a "system AI is disabled" message when the
    form's model field was still empty. Now the test prompt
    travels with the request body so unsaved tweaks can be
    validated.

  * **Model picker** replaces the free-text Model field. A
    Refresh button calls HubAdmin.ListSystemAiModels (provider's
    `/v1/models`); installed models show up as clickable chips
    that fill the field with one tap. The text input stays for
    cases where the operator already knows the id.

  * **Pull** button (Ollama only) downloads a model via
    `/api/pull`. Type `gemma3:4b`, click Pull, spinner, then the
    new model lights up in the chip list. No more "switch to a
    terminal to ollama-pull" detour.

Helper text updates for the empty-model case ("Required: pick
from the list (Refresh) or type one. Use Pull to download from
Ollama.") so a fresh operator never wonders why Save / Test
errors out.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-07 14:02:44 +02:00
parent 88c5f2da1f
commit 5b6641567c
4 changed files with 266 additions and 23 deletions

View file

@ -163,11 +163,24 @@ class HubService {
);
}
/// Probe the configured provider with a tiny ping. Same
/// error-kind taxonomy as [askAi]; "Test connection" buttons
/// use this to surface "is it working?".
Future<AskAiResult> testSystemAi() async {
final r = await _client.testSystemAi();
/// Probe a System-AI configuration with a tiny ping. Pass
/// the editor's current form values so "Test connection"
/// works *before* save. Empty fields fall back to the live
/// in-memory config.
Future<AskAiResult> testSystemAi({
String provider = '',
String endpoint = '',
String model = '',
String apiKeyEnv = '',
String privacyMode = '',
}) async {
final r = await _client.testSystemAi(
provider: provider,
endpoint: endpoint,
model: model,
apiKeyEnv: apiKeyEnv,
privacyMode: privacyMode,
);
return AskAiResult(
errorKind: r.errorKind,
text: r.text,
@ -175,6 +188,48 @@ class HubService {
);
}
/// List the models the provider exposes via `GET /v1/models`.
/// Studio's editor uses this to populate a dropdown instead
/// of forcing the operator to type the id manually.
Future<({String errorKind, String text, List<String> ids})>
listSystemAiModels({
String provider = '',
String endpoint = '',
String apiKeyEnv = '',
}) async {
final r = await _client.listSystemAiModels(
provider: provider,
endpoint: endpoint,
apiKeyEnv: apiKeyEnv,
);
return (
errorKind: r.errorKind,
text: r.text,
ids: r.models.map((m) => m.id).toList(),
);
}
/// Pull (download) an Ollama model via /api/pull.
/// Synchronous; can take minutes for large models. Errors
/// come back as `errorKind`.
Future<({String errorKind, String text, int elapsedMs})>
pullSystemAiModel({
required String endpoint,
required String model,
String apiKeyEnv = '',
}) async {
final r = await _client.pullSystemAiModel(
endpoint: endpoint,
model: model,
apiKeyEnv: apiKeyEnv,
);
return (
errorKind: r.errorKind,
text: r.text,
elapsedMs: r.elapsedMs,
);
}
/// Active channel + per-channel daemon status snapshot.
Future<ChannelStatusSnapshot> channelStatus() async {
final r = await _client.channelStatus();