Present agent creation above retained Agents list

Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-19 18:20:05 -07:00
parent 47a24bff23
commit a24eb029f8
2 changed files with 141 additions and 15 deletions

View File

@@ -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<void> 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<bool>(
context: context,
isScrollControlled: true,
showCloseButton: false,
builder: (_) => CreatePrototypeAgentSheet(
controller: controller,
community: community,
onCreated: () => ref.invalidate(agentDirectoryProvider),
),
),
: () async {
creating.value = true;
try {
await showBuzzModalBottomSheet<bool>(
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'),

View File

@@ -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<void>? creationGate;
@override
Future<void> ensureBinding() async {
owner = 'owner';
@@ -49,6 +52,12 @@ class _Controller extends RemoteAgentController {
Future<bool> 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<void>();
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<IconButton>(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<IconButton>(
find.widgetWithIcon(IconButton, Icons.check),
)
.onPressed,
isNotNull,
);
}
},
);
}
for (final succeed in [true, false]) {
testWidgets(
'Create returns to list only after confirmed success: $succeed',