diff --git a/mobile/README.md b/mobile/README.md index b46292f5c3..f47e6f004b 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -51,10 +51,9 @@ any length while the QR stays small. } ``` -- Every field is optional and left for the user when missing. Cards published - so far have only `name` and `avatarUrl`. snake_case and a few synonyms - (`system_prompt`, `model_id`, `effort`, a `model` object with an `id`) are - also read. Other fields are ignored. +- `name` and `avatarUrl` are on every published card; `instructions`, `model` + and `reasoningEffort` are optional. A missing field, or one that isn't a + non-blank string, is left for the user. Other keys are ignored. - The avatar is used only if it is an `https` URL on the same CDN; otherwise the form picks its usual default. It stays editable. - `model` and `reasoningEffort` are pre-selected only if the live model catalog diff --git a/mobile/lib/features/agents/agent_creation_draft.dart b/mobile/lib/features/agents/agent_creation_draft.dart index 016e804172..05585043f2 100644 --- a/mobile/lib/features/agents/agent_creation_draft.dart +++ b/mobile/lib/features/agents/agent_creation_draft.dart @@ -105,32 +105,18 @@ class AgentCreationDraft { return parseAgentCard(json, cdn); } - /// Reads a card's draft. Only `name`, `avatarUrl`, `instructions`, `model` - /// and `reasoningEffort` are defined; cards published before instructions - /// and model existed have just the first two. Any field may be missing or - /// spelled in snake_case or a common synonym, and is then left for the user. - /// An avatar is used only from [cdn], the host the card itself came from. + /// Reads a card's draft from the `agent.json` schema's `name`, + /// `avatarUrl`, `instructions`, `model` and `reasoningEffort`. Cards + /// published before instructions and model existed have just the first two. + /// A missing field, or one that isn't a non-blank string, is left for the + /// user; other keys are ignored. An avatar is used only from [cdn], the host + /// the card itself came from. static AgentCreationDraft parseAgentCard(Map json, Uri cdn) { - final model = json['model']; - final avatarUrl = _string(json, const [ - 'avatarUrl', - 'avatar_url', - 'avatar', - 'pictureUrl', - 'picture_url', - 'picture', - ]); + final avatarUrl = _string(json, 'avatarUrl'); final avatar = Uri.tryParse(avatarUrl ?? ''); return AgentCreationDraft( - name: _string(json, const ['name', 'displayName', 'display_name']) ?? '', - instructions: - _string(json, const [ - 'instructions', - 'systemPrompt', - 'system_prompt', - 'prompt', - ]) ?? - '', + name: _string(json, 'name') ?? '', + instructions: _string(json, 'instructions') ?? '', avatar: avatar != null && avatar.scheme == 'https' && @@ -139,30 +125,15 @@ class AgentCreationDraft { avatar.userInfo.isEmpty ? AgentAvatarSelection.url(avatarUrl!) : null, - // `model` may also be an object such as {"id": ..., "reasoningEffort": ...}. - modelId: model is Map - ? _string(model, const ['id', 'modelId', 'model_id', 'name']) - : _string(json, const ['model', 'modelId', 'model_id']), - reasoningEffort: - _string(json, _effortKeys) ?? - (model is Map ? _string(model, _effortKeys) : null), + modelId: _string(json, 'model'), + reasoningEffort: _string(json, 'reasoningEffort'), ); } - static const _effortKeys = [ - 'reasoningEffort', - 'reasoning_effort', - 'effort', - 'thinking', - ]; - - /// The first non-blank string among [keys], trimmed. - static String? _string(Map json, List keys) { - for (final key in keys) { - final value = json[key]; - if (value is String && value.trim().isNotEmpty) return value.trim(); - } - return null; + /// [key]'s value if it is a non-blank string, trimmed. + static String? _string(Map json, String key) { + final value = json[key]; + return value is String && value.trim().isNotEmpty ? value.trim() : null; } static Future> _readBounded(Stream> stream) async { diff --git a/mobile/test/features/agents/agent_creation_link_journey_test.dart b/mobile/test/features/agents/agent_creation_link_journey_test.dart index ed1e0a8e19..b187ccd56a 100644 --- a/mobile/test/features/agents/agent_creation_link_journey_test.dart +++ b/mobile/test/features/agents/agent_creation_link_journey_test.dart @@ -47,7 +47,7 @@ final _cards = >{ 'collection': 'gloopie', 'name': 'Owl', 'avatarUrl': '$_cdn/av_v1/r1/avatar.png', - 'model_id': 'retired-model', + 'model': 'retired-model', }, 'av_third': {'name': 'Third'}, 'av_fourth': {'name': 'Fourth'}, @@ -161,7 +161,7 @@ void main() { await tester.pumpAndSettle(); // A card as published today (no instructions, a model the catalog - // lacks, under another key): the draft opens with empty instructions + // lacks): the draft opens with empty instructions // and the model left for the user, so Create stays disabled. await receive(_link('av_v1')); expect(find.byType(CreateAgentPage), findsOneWidget); diff --git a/mobile/test/shared/deeplink/agent_creation_link_test.dart b/mobile/test/shared/deeplink/agent_creation_link_test.dart index b8280ebedd..78fd2c50c8 100644 --- a/mobile/test/shared/deeplink/agent_creation_link_test.dart +++ b/mobile/test/shared/deeplink/agent_creation_link_test.dart @@ -96,31 +96,19 @@ void main() { model: 'sonnet-5-5', effort: 'high', ), - 'snake_case and synonyms': ( + 'other key names are ignored': ( json: { - 'display_name': 'Scout', + 'name': 'Scout', 'avatar_url': avatar, 'system_prompt': 'Be kind.', 'model_id': 'opus-5-5', 'reasoning_effort': 'low', }, name: 'Scout', - instructions: 'Be kind.', - avatar: avatar, - model: 'opus-5-5', - effort: 'low', - ), - 'model object': ( - json: { - 'name': 'Scout', - 'prompt': 'Hi.', - 'model': {'id': 'opus-5-5', 'effort': 'medium'}, - }, - name: 'Scout', - instructions: 'Hi.', + instructions: '', avatar: null, - model: 'opus-5-5', - effort: 'medium', + model: null, + effort: null, ), 'wrong types and blanks are left for the user': ( json: {