Files
buzz/mobile/lib/shared/me/default_agent_writer.dart
klopez4212 344f007f36 Add mobile Bestie onboarding and fix remote agent presence (#8957)
An empty Bestie slot cycles still posters from Fuzzies, Gloopies and
Figgies every five seconds, without creating video players. The next
poster is precached; reduced motion and hidden-screen pauses are
preserved. The launcher uses the neutral “Bestie” label until a
selection is available, then “Chat with …”.

For supported communities with no owned agents, tapping opens Create
agent directly. A successful first-agent setup saves that agent as
Bestie without replacing an existing preference; existing-agent and
unsupported-community paths keep the Agents list.

Verified Builderlab registrations can show sleeping remote agents as
Online. Owner-scoped saved runtime availability now prevents a known
absent or deleted runtime from returning Online after Builderlab
sign-out or a fresh load, even when another community retains its
registration.

Validation: mobile static checks; 4,274 Flutter tests passed with hosted
flows enabled; launcher → cancel → create → Home “Chat with Scout”,
including encrypted persisted selection; sign-out, fresh-load, owner
isolation and deletion regressions. Restoring the old carousel produced
three video-player requests; removing the saved-availability guard
reproduced incorrect Online after sign-out.

The earlier iPhone recording below shows the original carousel and entry
flow and stops before creation. The new poster-only behavior and
completed creation are covered by widget tests, not a new device
recording.


https://github.com/user-attachments/assets/94fcff72-967a-44c2-99d5-27c715e196a2

---------

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

71 lines
2.0 KiB
Dart

import 'dart:convert';
import 'package:nostr/nostr.dart' as nostr;
import '../crypto/nip44.dart';
import '../relay/relay.dart';
import 'default_agent.dart';
import 'me_sync.dart';
/// Publishes and confirms the owner's encrypted, community-scoped choice.
/// Failures propagate to the selection UI so the user can retry.
Future<void> saveDefaultAgent(
MeSync sync,
String? pubkey, {
bool onlyIfUnset = false,
}) async {
if (pubkey != null && !RegExp(r'^[0-9a-f]{64}$').hasMatch(pubkey)) {
throw const FormatException('Invalid agent public key');
}
sync.check();
final filter = NostrFilter(
kinds: const [30078],
authors: [sync.pubkey],
tags: {
'#d': [defaultAgentCoordinate(sync.community)],
},
limit: 1,
);
final previous = await sync.session.fetchHistory(filter);
sync.check();
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
for (final event in previous) {
decodeDefaultAgent(event, sync);
if (onlyIfUnset) return;
if (event.createdAt >= now) {
throw StateError('Please wait a second before saving again.');
}
}
final event = NostrEvent.fromJson(
nostr.Event.from(
kind: 30078,
content: nip44Encrypt(
sync.key,
jsonEncode({'version': 1, 'agent': pubkey}),
),
secretKey: sync.privateHex,
createdAt: now,
tags: [
['d', defaultAgentCoordinate(sync.community)],
['t', defaultAgentTag],
],
verify: false,
).toMap(),
);
await sync.session.publish(event);
sync.check();
final confirmed = await sync.session.fetchHistory(filter);
sync.check();
if (confirmed.isEmpty || confirmed.first.id != event.id) {
throw StateError('The Bestie changed on another device. Try again.');
}
final prefs = sync.prefs;
if (prefs != null &&
!await prefs.setString(
'main-agent:${sync.community}:${sync.pubkey}',
jsonEncode(event.toJson()),
)) {
throw StateError('Bestie saved, but offline storage failed. Try again.');
}
}