Files
buzz/mobile/lib/features/agents/agent_model_picker.dart
klopez4212 a12ad1cdd2 feat(mobile): refine agent profiles and model selection (#8773)
## Summary
- Simplify agent profiles with Message/Bestie actions, relay-authored
published instructions (unavailable without a config channel),
management details, public-key copying, and a separate final Delete
section.
- Sort the Agents directory by name and mark the selected Bestie with a
muted filled star.
- Place Model and Thinking together below Name. Use native iOS Liquid
Glass menus and preserve selection callbacks as controls become enabled.

Related: #8685.

## Validation
Full mobile checks passed. Regression coverage includes relay-author
filtering, unavailable instructions, metadata retry, resizing native
pickers, accurate deletion state, selection after loading, and Bestie
switching. Real iOS simulator taps verified both pickers; the user
confirmed the earlier iPhone build works. Review fixes were checked with
widget journeys and the native UIKit resize/selection probe.

<details>
<summary>Previews</summary>

Synthetic fixture renders:

| Profile | Create agent | Bestie card |
| --- | --- | --- |
| ![Profile with separate Delete
section](https://github.com/user-attachments/assets/4b6e7cf5-b152-43a3-99d9-7c1ee65d0c70)
| ![Model and Thinking
settings](https://github.com/user-attachments/assets/c81dc138-d5d3-43bb-873b-a64171155a7d)
| ![Muted Bestie
star](https://github.com/user-attachments/assets/5ce95e3b-2403-41c5-b6d6-52b8b60cc7d6)
|

Native iOS simulator interaction with a synthetic catalog:

![Both picker values updated after native menu
selections](https://github.com/user-attachments/assets/a3ee6d4d-91e0-4df6-b4cf-b66d5c1a2e87)

</details>

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-31 11:28:14 -06:00

120 lines
4.0 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import '../../shared/agents/remote_agents.dart';
import '../../shared/theme/theme.dart';
import '../../shared/widgets/sheet_action_section.dart';
import '../../shared/widgets/buzz_setting_menu.dart';
/// Uses the server catalog for both menus, including model-specific efforts.
class AgentModelPicker extends StatelessWidget {
const AgentModelPicker({
required this.controller,
required this.catalog,
required this.onRetry,
required this.onChanged,
this.modelId,
this.effort,
super.key,
});
final RemoteAgentController controller;
final AsyncSnapshot<List<RemoteAgentModel>> catalog;
final VoidCallback onRetry;
final String? modelId;
final String? effort;
final void Function(String?, String?) onChanged;
@override
Widget build(BuildContext context) {
// While loading, show the same rows disabled so nothing shifts.
final loading = catalog.connectionState != ConnectionState.done;
if (!loading && (catalog.hasError || catalog.data!.isEmpty)) {
return Column(
children: [
Text(
catalog.hasError
? 'Could not load available models.'
: 'No models are available.',
),
TextButton(onPressed: onRetry, child: const Text('Retry models')),
],
);
}
final models = loading ? const <RemoteAgentModel>[] : catalog.data!;
final selected = models.where((model) => model.id == modelId).firstOrNull;
final validEffort = selected?.efforts.contains(effort) == true
? effort
: null;
final enabled = !loading && !controller.busy;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SheetActionSection(
dividerIndent: Grid.xs,
children: [
BuzzSettingMenu<String>(
controlKey: const Key('agent-model'),
enabled: enabled,
label: 'Model',
grouped: true,
trailingIcon: Icons.unfold_more,
onSelected: (value) => onChanged(value, null),
options: [
for (final model in models)
BuzzSettingMenuOption(label: model.name, value: model.id),
],
value: selected?.id,
valueLabel: loading ? 'Loading…' : selected?.name ?? 'Choose',
),
BuzzSettingMenu<String>(
controlKey: const Key('agent-effort'),
enabled: enabled && selected != null,
label: 'Thinking',
grouped: true,
trailingIcon: Icons.unfold_more,
onSelected: (value) => onChanged(selected!.id, value),
options: [
for (final value in selected?.efforts ?? <String>[])
BuzzSettingMenuOption(label: value, value: value),
],
value: validEffort,
valueLabel: loading ? 'Loading…' : validEffort ?? 'Choose',
),
],
),
if (modelId != null && selected == null ||
effort != null && validEffort == null)
const Text(
'Choose an available model and supported reasoning effort.',
),
],
);
}
}
/// Shares catalog loading with the submit action so stale selections cannot save.
(AsyncSnapshot<List<RemoteAgentModel>>, VoidCallback) useAgentModelCatalog(
RemoteAgentController controller,
) {
final retry = useState(0);
final future = useMemoized(controller.modelCatalog, [
controller,
retry.value,
]);
return (useFuture(future, preserveState: false), () => retry.value++);
}
/// True only after the loaded catalog confirms this exact pair.
bool validAgentModelSelection(
AsyncSnapshot<List<RemoteAgentModel>> catalog,
String? model,
String? effort,
) =>
catalog.connectionState == ConnectionState.done &&
!catalog.hasError &&
catalog.data?.any(
(entry) => entry.id == model && entry.efforts.contains(effort),
) ==
true;