From a24eb029f8bd39ff73f81da0b30275da54c64d81 Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Mon, 19 Oct 2026 18:20:05 -0700 Subject: [PATCH] Present agent creation above retained Agents list Signed-off-by: Tom Brow --- mobile/lib/prototype/agents_sheet.dart | 42 +++++--- mobile/test/prototype/agents_ui_test.dart | 114 ++++++++++++++++++++++ 2 files changed, 141 insertions(+), 15 deletions(-) diff --git a/mobile/lib/prototype/agents_sheet.dart b/mobile/lib/prototype/agents_sheet.dart index 338f66c086..03dfc39586 100644 --- a/mobile/lib/prototype/agents_sheet.dart +++ b/mobile/lib/prototype/agents_sheet.dart @@ -164,11 +164,13 @@ class PrototypeAgentsSheet extends HookConsumerWidget { ); keys.sort((a, b) => names.labelFor(a).compareTo(names.labelFor(b))); final profiles = ref.watch(userCacheProvider); + final creating = useState(false); final failed = directory.hasError || registered.hasError; final loading = - controller.busy || - directory.isLoading || - registered.connectionState != ConnectionState.done; + !creating.value && + (controller.busy || + directory.isLoading || + registered.connectionState != ConnectionState.done); Future refresh() async { await controller.run(controller.refresh); @@ -186,18 +188,26 @@ class PrototypeAgentsSheet extends HookConsumerWidget { ), trailing: IconButton( tooltip: 'Add agent', - onPressed: community == null || controller.busy + onPressed: community == null || controller.busy || creating.value ? null - : () => showBuzzModalBottomSheet( - context: context, - isScrollControlled: true, - showCloseButton: false, - builder: (_) => CreatePrototypeAgentSheet( - controller: controller, - community: community, - onCreated: () => ref.invalidate(agentDirectoryProvider), - ), - ), + : () async { + creating.value = true; + try { + await showBuzzModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + showCloseButton: false, + builder: (_) => CreatePrototypeAgentSheet( + controller: controller, + community: community, + onCreated: () => ref.invalidate(agentDirectoryProvider), + ), + ); + } finally { + if (context.mounted) creating.value = false; + } + }, icon: const Icon(Icons.add), ), child: Material( @@ -212,7 +222,9 @@ class PrototypeAgentsSheet extends HookConsumerWidget { if (controller.account.account == null) ListTile( title: const Text('Sign in to create an agent'), - subtitle: controller.busy ? Text(controller.status) : null, + subtitle: controller.busy && !creating.value + ? Text(controller.status) + : null, trailing: TextButton( onPressed: controller.busy ? null : controller.signIn, child: const Text('Sign in'), diff --git a/mobile/test/prototype/agents_ui_test.dart b/mobile/test/prototype/agents_ui_test.dart index caf0199968..42a0babf5c 100644 --- a/mobile/test/prototype/agents_ui_test.dart +++ b/mobile/test/prototype/agents_ui_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:buzz/prototype/agents_sheet.dart'; import 'package:buzz/features/profile/user_profile_sheet.dart'; import 'package:buzz/shared/mentions/agent_identity_provider.dart'; @@ -35,6 +37,7 @@ class _Controller extends RemoteAgentController { String? submittedName; String? submittedInstructions; bool succeed = true; + Completer? creationGate; @override Future ensureBinding() async { owner = 'owner'; @@ -49,6 +52,12 @@ class _Controller extends RemoteAgentController { Future create(String name, {String instructions = ''}) async { submittedName = name; submittedInstructions = instructions; + if (creationGate case final gate?) { + return run(() async { + await gate.future; + if (!succeed) throw StateError('Creation failed. Retry.'); + }); + } status = 'Creation failed. Retry.'; return succeed; } @@ -181,6 +190,111 @@ void main() { expect(find.byType(BottomSheet), findsNothing); }); + for (final succeeds in [true, false]) { + testWidgets( + 'creation overlays retained list; only top modal progresses: $succeeds', + (tester) async { + final account = BuilderlabAccountClient(); + final gate = Completer(); + final controller = _Controller(account) + ..succeed = succeeds + ..creationGate = gate; + addTearDown(() { + controller.dispose(); + controller.relayAuth.dispose(); + account.dispose(); + }); + final existing = 'a' * 64; + final added = 'b' * 64; + final community = Community.create( + name: 'Test', + relayUrl: 'wss://test.example', + pubkey: 'owner', + ); + var completed = false; + await tester.pumpWidget( + ProviderScope( + overrides: [ + activeCommunityProvider.overrideWith((ref) async => community), + agentDirectoryProvider.overrideWith( + (ref) async => [ + AgentDirectoryEntry(pubkey: existing), + if (completed && succeeds) AgentDirectoryEntry(pubkey: added), + ], + ), + ], + child: MaterialApp( + theme: AppTheme.light(), + home: Builder( + builder: (context) => Scaffold( + body: TextButton( + onPressed: () => showPrototypeAgents(context, controller), + child: const Text('Open Agents'), + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('Open Agents')); + await tester.pumpAndSettle(); + final list = tester.element(find.byType(PrototypeAgentsSheet)); + await tester.tap(find.byTooltip('Add agent')); + await tester.pumpAndSettle(); + expect(find.byType(BottomSheet), findsNWidgets(2)); + expect(list.mounted, isTrue); + final createContext = tester.element( + find.byType(CreatePrototypeAgentSheet), + ); + expect( + Navigator.of(createContext), + Navigator.of(createContext, rootNavigator: true), + ); + expect(Navigator.of(list), isNot(Navigator.of(createContext))); + await tester.enterText(find.widgetWithText(TextField, 'Name'), 'Scout'); + await tester.pump(); + await tester.tap(find.byTooltip('Create')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + expect(controller.busy, isTrue); + expect(find.byType(LinearProgressIndicator), findsOneWidget); + expect( + find.descendant( + of: find.byType(PrototypeAgentsSheet), + matching: find.byType(LinearProgressIndicator), + ), + findsNothing, + ); + expect(find.byKey(ValueKey(existing)), findsOneWidget); + expect( + tester + .widget(find.widgetWithIcon(IconButton, Icons.check)) + .onPressed, + isNull, + ); + completed = true; + gate.complete(); + await tester.pumpAndSettle(); + expect(find.byType(BottomSheet), findsNWidgets(succeeds ? 1 : 2)); + expect(tester.element(find.byType(PrototypeAgentsSheet)), same(list)); + if (succeeds) { + expect(find.byKey(ValueKey(added)), findsOneWidget); + } else { + expect(find.text('Creation failed. Retry.'), findsOneWidget); + expect( + tester + .widget( + find.widgetWithIcon(IconButton, Icons.check), + ) + .onPressed, + isNotNull, + ); + } + }, + ); + } + for (final succeed in [true, false]) { testWidgets( 'Create returns to list only after confirmed success: $succeed',