mirror of
https://github.com/block/buzz.git
synced 2026-11-06 09:21:45 +00:00
mobile: start new agents on Sonnet at medium effort
The create screen opened with model and effort blank, so the easy path was picking from the full catalog, expensive models included. A fresh setup now starts on the first Sonnet model the catalog offers, at medium effort when supported. A draft link's own model still wins when offered, a resumed setup keeps its saved choice, and anything the catalog lacks is left for the user to choose. Co-authored-by: Carl <c217fe6b9d958f41c3a5e030dccc7f626775a923089cb6491305eade75ea1f1b@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
@@ -117,3 +117,27 @@ bool validAgentModelSelection(
|
|||||||
(entry) => entry.id == model && entry.efforts.contains(effort),
|
(entry) => entry.id == model && entry.efforts.contains(effort),
|
||||||
) ==
|
) ==
|
||||||
true;
|
true;
|
||||||
|
|
||||||
|
/// The model and effort a new agent starts with, so the easy path is a
|
||||||
|
/// capable but affordable choice rather than a blank (or the priciest) one.
|
||||||
|
/// A draft's own model wins when the catalog offers it; otherwise the first
|
||||||
|
/// Sonnet model, at medium effort when supported. Anything the catalog
|
||||||
|
/// doesn't offer is left for the user to choose.
|
||||||
|
(String?, String?) startingAgentModel(
|
||||||
|
List<RemoteAgentModel> catalog, {
|
||||||
|
String? draftModel,
|
||||||
|
String? draftEffort,
|
||||||
|
}) {
|
||||||
|
final drafted = catalog.where((entry) => entry.id == draftModel).firstOrNull;
|
||||||
|
if (drafted != null) {
|
||||||
|
return (
|
||||||
|
drafted.id,
|
||||||
|
drafted.efforts.contains(draftEffort) ? draftEffort : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final fallback = catalog
|
||||||
|
.where((entry) => entry.id.toLowerCase().contains('sonnet'))
|
||||||
|
.firstOrNull;
|
||||||
|
if (fallback == null) return (null, null);
|
||||||
|
return (fallback.id, fallback.efforts.contains('medium') ? 'medium' : null);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import 'agent_avatar_hero.dart';
|
import 'agent_avatar_hero.dart';
|
||||||
import 'agent_character_artwork.dart';
|
import 'agent_character_artwork.dart';
|
||||||
|
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'agent_model_picker.dart';
|
import 'agent_model_picker.dart';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
@@ -57,26 +60,25 @@ class CreateAgentPage extends HookConsumerWidget {
|
|||||||
final draft = controller.pending == null ? initialDraft : null;
|
final draft = controller.pending == null ? initialDraft : null;
|
||||||
final model = useState(controller.pending?['model_id'] as String?);
|
final model = useState(controller.pending?['model_id'] as String?);
|
||||||
final effort = useState(controller.pending?['reasoning_effort'] as String?);
|
final effort = useState(controller.pending?['reasoning_effort'] as String?);
|
||||||
// Draft model hints apply to the first catalog that loads (after a failed
|
// A fresh setup starts from the first catalog that loads (after a failed
|
||||||
// load, the one Retry fetches), and only if it offers them.
|
// load, the one Retry fetches): the draft's model if offered, otherwise
|
||||||
final hintsApplied = useRef(false);
|
// the default. A resumed setup keeps its saved choice.
|
||||||
|
final startApplied = useRef(controller.pending != null);
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
final entries = catalog.data;
|
final entries = catalog.data;
|
||||||
if (draft == null ||
|
if (startApplied.value ||
|
||||||
hintsApplied.value ||
|
|
||||||
catalog.connectionState != ConnectionState.done ||
|
catalog.connectionState != ConnectionState.done ||
|
||||||
entries == null) {
|
entries == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
hintsApplied.value = true;
|
startApplied.value = true;
|
||||||
final match = entries
|
final (startModel, startEffort) = startingAgentModel(
|
||||||
.where((entry) => entry.id == draft.modelId)
|
entries,
|
||||||
.firstOrNull;
|
draftModel: draft?.modelId,
|
||||||
if (match == null) return null;
|
draftEffort: draft?.reasoningEffort,
|
||||||
model.value = match.id;
|
);
|
||||||
effort.value = match.efforts.contains(draft.reasoningEffort)
|
model.value = startModel;
|
||||||
? draft.reasoningEffort
|
effort.value = startEffort;
|
||||||
: null;
|
|
||||||
return null;
|
return null;
|
||||||
}, [catalog.connectionState]);
|
}, [catalog.connectionState]);
|
||||||
final name = useTextEditingController(
|
final name = useTextEditingController(
|
||||||
|
|||||||
66
mobile/test/features/agents/agent_model_picker_test.dart
Normal file
66
mobile/test/features/agents/agent_model_picker_test.dart
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import 'package:buzz/features/agents/agent_model_picker.dart';
|
||||||
|
import 'package:buzz/shared/agents/remote_agents.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
const opus = RemoteAgentModel(
|
||||||
|
id: 'kgoose-claude-opus-4-6',
|
||||||
|
name: 'Opus',
|
||||||
|
efforts: ['low', 'medium', 'high'],
|
||||||
|
);
|
||||||
|
const sonnet = RemoteAgentModel(
|
||||||
|
id: 'kgoose-claude-sonnet-4-6',
|
||||||
|
name: 'Sonnet',
|
||||||
|
efforts: ['low', 'medium', 'high'],
|
||||||
|
);
|
||||||
|
|
||||||
|
test('defaults to Sonnet at medium effort', () {
|
||||||
|
expect(startingAgentModel([opus, sonnet]), (
|
||||||
|
'kgoose-claude-sonnet-4-6',
|
||||||
|
'medium',
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('leaves effort for the user when medium is not offered', () {
|
||||||
|
const highOnly = RemoteAgentModel(
|
||||||
|
id: 'claude-sonnet-5',
|
||||||
|
name: 'Sonnet 5',
|
||||||
|
efforts: ['high'],
|
||||||
|
);
|
||||||
|
expect(startingAgentModel([opus, highOnly]), ('claude-sonnet-5', null));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('chooses nothing when no Sonnet is offered', () {
|
||||||
|
expect(startingAgentModel([opus]), (null, null));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a draft model the catalog offers wins over the default', () {
|
||||||
|
expect(
|
||||||
|
startingAgentModel(
|
||||||
|
[opus, sonnet],
|
||||||
|
draftModel: opus.id,
|
||||||
|
draftEffort: 'high',
|
||||||
|
),
|
||||||
|
(opus.id, 'high'),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
startingAgentModel(
|
||||||
|
[opus, sonnet],
|
||||||
|
draftModel: opus.id,
|
||||||
|
draftEffort: 'max',
|
||||||
|
),
|
||||||
|
(opus.id, null),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a draft model the catalog lacks falls back to the default', () {
|
||||||
|
expect(
|
||||||
|
startingAgentModel(
|
||||||
|
[opus, sonnet],
|
||||||
|
draftModel: 'gone',
|
||||||
|
draftEffort: 'high',
|
||||||
|
),
|
||||||
|
(sonnet.id, 'medium'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:buzz/shared/widgets/frosted_app_bar.dart';
|
import 'package:buzz/shared/widgets/frosted_app_bar.dart';
|
||||||
import 'package:buzz/shared/widgets/preset_agent_avatar.dart';
|
import 'package:buzz/shared/widgets/preset_agent_avatar.dart';
|
||||||
import 'package:buzz/shared/agents/agent_avatar_catalog.dart';
|
import 'package:buzz/shared/agents/agent_avatar_catalog.dart';
|
||||||
import 'package:buzz/features/agents/agent_avatar_editor_page.dart';
|
import 'package:buzz/features/agents/agent_avatar_editor_page.dart';
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:buzz/shared/agents/remote_agents.dart';
|
import 'package:buzz/shared/agents/remote_agents.dart';
|
||||||
@@ -228,6 +230,55 @@ void main() {
|
|||||||
expect(h.registry.calls, contains('publish-policy'));
|
expect(h.registry.calls, contains('publish-policy'));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets('a new agent starts on the affordable default model', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final h = await AgentsHarness.create();
|
||||||
|
h.registry.models = [
|
||||||
|
{
|
||||||
|
'id': 'premium-model',
|
||||||
|
'display_name': 'Premium model',
|
||||||
|
'supported_reasoning_efforts': ['low', 'medium', 'high'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'kgoose-claude-sonnet-4-6',
|
||||||
|
'display_name': 'Sonnet',
|
||||||
|
'supported_reasoning_efforts': ['low', 'medium', 'high'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await h.open(tester, directory: [existing]);
|
||||||
|
if (!supportsRemoteAgents(h.community)) return;
|
||||||
|
await tester.tap(find.byTooltip('Add agent'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Sonnet'), findsOneWidget);
|
||||||
|
expect(find.text('Premium model'), findsNothing);
|
||||||
|
expect(find.text('medium'), findsOneWidget);
|
||||||
|
|
||||||
|
// Naming it is all that's left before Create.
|
||||||
|
await tester.enterText(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byWidgetPredicate(
|
||||||
|
(widget) => widget is Semantics && widget.properties.label == 'Name',
|
||||||
|
),
|
||||||
|
matching: find.byType(TextField),
|
||||||
|
),
|
||||||
|
'Scout',
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
expect(
|
||||||
|
tester
|
||||||
|
.widget<IconButton>(
|
||||||
|
find.ancestor(
|
||||||
|
of: find.byTooltip('Create'),
|
||||||
|
matching: find.byType(IconButton),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.onPressed,
|
||||||
|
isNotNull,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StalledPosterClient implements HttpClient {
|
class _StalledPosterClient implements HttpClient {
|
||||||
|
|||||||
Reference in New Issue
Block a user