Preserve agent activity across thread scopes

Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
kenny lopez
2026-08-17 17:20:02 +01:00
parent 7da997a567
commit d5b96f9a27
4 changed files with 76 additions and 13 deletions

View File

@@ -155,17 +155,28 @@ final composerActivityStateProvider = Provider.autoDispose
/// Agent pubkeys working in a channel, for the members badge and sheet.
final workingBotPubkeysProvider = Provider.autoDispose
.family<Set<String>, String>((ref, channelId) {
final activity = ref.watch(
composerActivityStateProvider((
channelId: channelId,
threadHeadId: null,
)),
);
return Set.unmodifiable(
activity.agents
.where((agent) => agent.isWorking)
.map((agent) => agent.pubkey),
);
final threadScopes = <String?>{
null,
for (final entry in ref.watch(channelTypingProvider(channelId)))
entry.threadHeadId,
for (final turn in ref.watch(composerAgentTurnStatesProvider))
if (turn.channelId == channelId) turn.threadHeadId,
};
final working = <String>{};
for (final threadHeadId in threadScopes) {
final activity = ref.watch(
composerActivityStateProvider((
channelId: channelId,
threadHeadId: threadHeadId,
)),
);
working.addAll(
activity.agents
.where((agent) => agent.isWorking)
.map((agent) => agent.pubkey),
);
}
return Set.unmodifiable(working);
});
int _compareComposerTurnRecency(AgentTurnState a, AgentTurnState b) {

View File

@@ -724,7 +724,7 @@ class ThreadDetailPage extends HookConsumerWidget {
if (!isMember || isArchived)
_ThreadTypingIndicator(
channelId: channelId,
threadHeadId: threadHead.id,
threadHeadId: effectiveRootId,
animated: false,
overlayTopBoundary: frostedAppBarHeight(context),
),
@@ -751,7 +751,7 @@ class ThreadDetailPage extends HookConsumerWidget {
restoreComposerFocus,
) => _ThreadTypingIndicator(
channelId: channelId,
threadHeadId: threadHead.id,
threadHeadId: effectiveRootId,
horizontalInset: 0,
overlayTopBoundary: frostedAppBarHeight(context),
compactWidthFactor: 0.85,

View File

@@ -259,6 +259,52 @@ void main() {
expect(container.read(workingBotPubkeysProvider(_channelId)), isEmpty);
});
test('channel working set includes agents from every thread scope', () {
final container = ProviderContainer(
overrides: [
currentPubkeyProvider.overrideWith((ref) => 'owner'),
channelMembersProvider(
_channelId,
).overrideWith((ref) async => const <ChannelMember>[]),
channelTypingProvider(_channelId).overrideWith(
() => _FakeTypingNotifier(const [
TypingEntry(
pubkey: 'agent-a',
threadHeadId: 'thread-1',
expiresAtMs: 9999999999999,
),
TypingEntry(
pubkey: 'human',
threadHeadId: 'thread-3',
expiresAtMs: 9999999999999,
),
]),
),
agentMentionPubkeysProvider(
_channelId,
).overrideWith((ref) => const {'agent-a', 'agent-b'}),
agentOwnersProvider.overrideWithValue(
const AsyncData({'agent-a': 'owner', 'agent-b': 'owner'}),
),
userCacheProvider.overrideWith(_FakeUserCacheNotifier.new),
observerRelayProvider.overrideWith(
() => _FakeObserverRelayNotifier({
'agent-b': [_observerFrame('agent-b')],
}),
),
composerAgentTurnStatesProvider.overrideWithValue([
_turn('agent-b', threadHeadId: 'thread-2'),
]),
],
);
addTearDown(container.dispose);
expect(container.read(workingBotPubkeysProvider(_channelId)), {
'agent-a',
'agent-b',
});
});
test('prefers live work over retained terminal history', () {
final receiptAt = DateTime.utc(2026, 8, 16, 12, 0, 5);
final container = ProviderContainer(

View File

@@ -14,6 +14,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:buzz/features/channels/channel.dart';
import 'package:buzz/features/channels/agent_activity/active_agent_turns.dart';
import 'package:buzz/features/channels/agent_activity/composer_agent_activity_indicator.dart';
import 'package:buzz/features/channels/agent_activity/observer_models.dart';
import 'package:buzz/features/channels/agent_activity/observer_subscription.dart';
import 'package:buzz/features/channels/agent_activity/working_bots_provider.dart';
@@ -4561,6 +4562,11 @@ void main() {
expect(threadPage.threadHead.id, 'parent');
expect(threadPage.initialMessageId, 'target');
final activityIndicator = tester.widget<ComposerAgentActivityIndicator>(
find.byType(ComposerAgentActivityIndicator),
);
expect(activityIndicator.threadHeadId, 'root');
final highlighted = tester.widget<DecoratedBox>(
find.byKey(const ValueKey('thread-message-target')),
);