mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Fix retained mobile activity labels
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -495,6 +495,7 @@ class ComposerAgentActivityIndicator extends HookConsumerWidget {
|
||||
observerState: observerState,
|
||||
transcript: transcript,
|
||||
profiles: profiles,
|
||||
signals: viewableWorking,
|
||||
selectorAgents: selectorAgents,
|
||||
selectedAgent: effectiveSelectedAgent,
|
||||
nameFor: nameFor,
|
||||
@@ -619,6 +620,7 @@ class _InlineActivityPanel extends StatelessWidget {
|
||||
final ObserverState? observerState;
|
||||
final List<TranscriptItem> transcript;
|
||||
final Map<String, UserProfile> profiles;
|
||||
final List<WorkingAgentSignal> signals;
|
||||
final List<String> selectorAgents;
|
||||
final String selectedAgent;
|
||||
final String Function(String) nameFor;
|
||||
@@ -639,6 +641,7 @@ class _InlineActivityPanel extends StatelessWidget {
|
||||
required this.observerState,
|
||||
required this.transcript,
|
||||
required this.profiles,
|
||||
required this.signals,
|
||||
required this.selectorAgents,
|
||||
required this.selectedAgent,
|
||||
required this.nameFor,
|
||||
@@ -655,6 +658,7 @@ class _InlineActivityPanel extends StatelessWidget {
|
||||
final headline = _selectedActivityHeadline(selectedTurn, transcript);
|
||||
final compactLabel = _agentActivityLabel(
|
||||
pubkeys: selectorAgents,
|
||||
signals: signals,
|
||||
selectedTurn: selectedTurn,
|
||||
transcript: transcript,
|
||||
nameFor: nameFor,
|
||||
|
||||
+34
-2
@@ -229,6 +229,7 @@ class _AgentActivityControl extends StatelessWidget {
|
||||
: [?selectedAgent];
|
||||
final label = _agentActivityLabel(
|
||||
pubkeys: pubkeys,
|
||||
signals: signals,
|
||||
selectedTurn: selectedTurn,
|
||||
transcript: transcript,
|
||||
nameFor: nameFor,
|
||||
@@ -393,16 +394,47 @@ _ActivityStatus _activityStatus(AgentTurnState? turn, bool isFallbackWorking) {
|
||||
|
||||
({String visibleLabel, String semanticLabel}) _agentActivityLabel({
|
||||
required List<String> pubkeys,
|
||||
required List<WorkingAgentSignal> signals,
|
||||
required AgentTurnState? selectedTurn,
|
||||
required List<TranscriptItem> transcript,
|
||||
required String Function(String) nameFor,
|
||||
required bool expanded,
|
||||
}) {
|
||||
final action = expanded ? 'Collapse live activity.' : 'Show live activity.';
|
||||
if (pubkeys.length > 1 && signals.length == pubkeys.length) {
|
||||
final working = signals.where((signal) => signal.isWorking).length;
|
||||
final errors = signals
|
||||
.where((signal) => signal.phase == AgentTurnPhase.error)
|
||||
.length;
|
||||
final finished = signals.length - working - errors;
|
||||
if (working == signals.length) {
|
||||
return (
|
||||
visibleLabel: '${signals.length} agents are working…',
|
||||
semanticLabel: '${signals.length} agents are working. $action',
|
||||
);
|
||||
}
|
||||
final visibleParts = [
|
||||
if (working > 0) '$working working',
|
||||
if (finished > 0) '$finished finished',
|
||||
if (errors > 0) '$errors ${errors == 1 ? 'error' : 'errors'}',
|
||||
];
|
||||
final semanticParts = [
|
||||
if (working > 0)
|
||||
'$working ${working == 1 ? 'agent is' : 'agents are'} working',
|
||||
if (finished > 0)
|
||||
'$finished ${finished == 1 ? 'agent has' : 'agents have'} finished',
|
||||
if (errors > 0)
|
||||
'$errors ${errors == 1 ? 'agent stopped' : 'agents stopped'} with ${errors == 1 ? 'an error' : 'errors'}',
|
||||
];
|
||||
return (
|
||||
visibleLabel: visibleParts.join(' · '),
|
||||
semanticLabel: '${semanticParts.join(', ')}. $action',
|
||||
);
|
||||
}
|
||||
if (pubkeys.length > 1) {
|
||||
return (
|
||||
visibleLabel: '${pubkeys.length} agents are working…',
|
||||
semanticLabel: '${pubkeys.length} agents are working. $action',
|
||||
visibleLabel: '${pubkeys.length} agents have activity',
|
||||
semanticLabel: '${pubkeys.length} agents have activity. $action',
|
||||
);
|
||||
}
|
||||
final name = pubkeys.isEmpty ? 'Agent' : nameFor(pubkeys.single);
|
||||
|
||||
@@ -22,6 +22,9 @@ class WorkingAgentSignal {
|
||||
|
||||
/// Whether this signal represents live work rather than a retained outcome.
|
||||
final bool isWorking;
|
||||
|
||||
/// Known observer phase; typing-only signals have no lifecycle phase yet.
|
||||
final AgentTurnPhase? phase;
|
||||
final String? turnId;
|
||||
final DateTime? startedAt;
|
||||
|
||||
@@ -30,6 +33,7 @@ class WorkingAgentSignal {
|
||||
required this.source,
|
||||
required this.canViewActivity,
|
||||
this.isWorking = true,
|
||||
this.phase,
|
||||
this.turnId,
|
||||
this.startedAt,
|
||||
});
|
||||
@@ -105,6 +109,7 @@ final composerActivityStateProvider = Provider.autoDispose
|
||||
source: AgentWorkingSource.observer,
|
||||
canViewActivity: canView(entry.key),
|
||||
isWorking: turn.isWorking,
|
||||
phase: turn.phase,
|
||||
turnId: turn.turnId,
|
||||
startedAt: turn.startedAt,
|
||||
);
|
||||
@@ -121,13 +126,15 @@ final composerActivityStateProvider = Provider.autoDispose
|
||||
}
|
||||
if (signals.containsKey(pubkey)) continue;
|
||||
final turn = activeByAgent[pubkey];
|
||||
final liveTurn = turn?.isWorking == true ? turn : null;
|
||||
signals[pubkey] = WorkingAgentSignal(
|
||||
pubkey: pubkey,
|
||||
source: AgentWorkingSource.typing,
|
||||
canViewActivity: canView(pubkey),
|
||||
isWorking: true,
|
||||
turnId: turn?.turnId,
|
||||
startedAt: turn?.startedAt,
|
||||
phase: liveTurn?.phase,
|
||||
turnId: liveTurn?.turnId,
|
||||
startedAt: liveTurn?.startedAt,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+99
@@ -206,6 +206,105 @@ void main() {
|
||||
expect(find.text('Thinking'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('summarizes mixed working and terminal agent states', (
|
||||
tester,
|
||||
) async {
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
composerActivityStateProvider(_scope).overrideWithValue(
|
||||
const ComposerActivityState(
|
||||
agents: [
|
||||
WorkingAgentSignal(
|
||||
pubkey: _agentPubkey,
|
||||
source: AgentWorkingSource.observer,
|
||||
canViewActivity: true,
|
||||
phase: AgentTurnPhase.working,
|
||||
turnId: _turnId,
|
||||
),
|
||||
WorkingAgentSignal(
|
||||
pubkey: _secondAgentPubkey,
|
||||
source: AgentWorkingSource.observer,
|
||||
canViewActivity: true,
|
||||
isWorking: false,
|
||||
phase: AgentTurnPhase.error,
|
||||
turnId: _secondTurnId,
|
||||
),
|
||||
],
|
||||
humanTyping: [],
|
||||
),
|
||||
),
|
||||
agentTurnStatesProvider.overrideWithValue([
|
||||
_turn(AgentTurnPhase.working),
|
||||
_turnFor(
|
||||
pubkey: _secondAgentPubkey,
|
||||
turnId: _secondTurnId,
|
||||
phase: AgentTurnPhase.error,
|
||||
),
|
||||
]),
|
||||
observerTurnSubscriptionProvider(
|
||||
_turnKey,
|
||||
).overrideWithValue(_observerState),
|
||||
userCacheProvider.overrideWith(_FakeUserCacheNotifier.new),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await tester.pumpWidget(_app(container));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('1 working · 1 error'), findsOneWidget);
|
||||
expect(find.text('2 agents are working…'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('summarizes multiple retained terminal outcomes', (tester) async {
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
composerActivityStateProvider(_scope).overrideWithValue(
|
||||
const ComposerActivityState(
|
||||
agents: [
|
||||
WorkingAgentSignal(
|
||||
pubkey: _agentPubkey,
|
||||
source: AgentWorkingSource.observer,
|
||||
canViewActivity: true,
|
||||
isWorking: false,
|
||||
phase: AgentTurnPhase.finished,
|
||||
turnId: _turnId,
|
||||
),
|
||||
WorkingAgentSignal(
|
||||
pubkey: _secondAgentPubkey,
|
||||
source: AgentWorkingSource.observer,
|
||||
canViewActivity: true,
|
||||
isWorking: false,
|
||||
phase: AgentTurnPhase.error,
|
||||
turnId: _secondTurnId,
|
||||
),
|
||||
],
|
||||
humanTyping: [],
|
||||
),
|
||||
),
|
||||
agentTurnStatesProvider.overrideWithValue([
|
||||
_turn(AgentTurnPhase.finished),
|
||||
_turnFor(
|
||||
pubkey: _secondAgentPubkey,
|
||||
turnId: _secondTurnId,
|
||||
phase: AgentTurnPhase.error,
|
||||
),
|
||||
]),
|
||||
observerTurnSubscriptionProvider(
|
||||
_turnKey,
|
||||
).overrideWithValue(_observerState),
|
||||
userCacheProvider.overrideWith(_FakeUserCacheNotifier.new),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await tester.pumpWidget(_app(container));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('1 finished · 1 error'), findsOneWidget);
|
||||
expect(find.text('2 agents are working…'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('reduced motion makes inline size changes immediate', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
@@ -114,6 +114,56 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test('thread typing does not inherit a retained terminal turn', () {
|
||||
final failedTurn = _turn('agent-a', phase: AgentTurnPhase.error);
|
||||
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,
|
||||
),
|
||||
]),
|
||||
),
|
||||
agentMentionPubkeysProvider(
|
||||
_channelId,
|
||||
).overrideWith((ref) => const {'agent-a'}),
|
||||
agentOwnersProvider.overrideWithValue(
|
||||
const AsyncData({'agent-a': 'owner'}),
|
||||
),
|
||||
userCacheProvider.overrideWith(_FakeUserCacheNotifier.new),
|
||||
observerRelayProvider.overrideWith(
|
||||
() => _FakeObserverRelayNotifier({
|
||||
'agent-a': [_observerFrame('agent-a')],
|
||||
}),
|
||||
),
|
||||
composerAgentTurnStatesProvider.overrideWithValue([failedTurn]),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final signal = container
|
||||
.read(
|
||||
composerActivityStateProvider((
|
||||
channelId: _channelId,
|
||||
threadHeadId: 'thread-1',
|
||||
)),
|
||||
)
|
||||
.agents
|
||||
.single;
|
||||
|
||||
expect(signal.source, AgentWorkingSource.typing);
|
||||
expect(signal.isWorking, isTrue);
|
||||
expect(signal.turnId, isNull);
|
||||
expect(signal.startedAt, isNull);
|
||||
});
|
||||
|
||||
test('keeps a recent owned error reachable after typing stops', () {
|
||||
final failedTurn = _turn('agent-a', phase: AgentTurnPhase.error);
|
||||
final container = ProviderContainer(
|
||||
|
||||
Reference in New Issue
Block a user