feat: add "Requires Attention" agent grouping feature

Implement a new UI section that surfaces agents requiring user attention at the top of the agent list. This includes agents that are running, have pending permissions, or have errors.

**Backend changes:**
- Add `requiresAttention` field to agent projections and SDK types
- Implement logic in AgentManager to detect attention states (running, pending permissions, errors)
- Add WebSocket message handler to notify clients of attention state changes
- Update session state to include attention tracking

**Frontend changes:**
- Add "REQUIRES ATTENTION" section in agent-list component with visual prominence
- Update useAggregatedAgents hook to expose `requiresAttention` agents
- Enhance session store to track and update attention states
- Add real-time updates via WebSocket for attention state changes
- Update type definitions across agent directory and stream types

Verified working via Playwright MCP testing on localhost:8081.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-12-04 11:27:08 +00:00
co-authored by Claude
parent fd36c9b8e2
commit 0f1758709d
16 changed files with 305 additions and 58 deletions
@@ -125,6 +125,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
on: () => () => {},
sendPing: () => {},
sendUserMessage: () => {},
clearAgentAttention: () => {},
subscribeConnectionStatus: () => () => {},
getConnectionState: () => ({ isConnected: false, isConnecting: false }),
}),
+116 -58
View File
@@ -1,5 +1,5 @@
import { View, Text, Pressable, FlatList, Modal, RefreshControl, type ListRenderItem } from "react-native";
import { useCallback, useState } from "react";
import { View, Text, Pressable, SectionList, Modal, RefreshControl, type SectionListRenderItem } from "react-native";
import { useCallback, useState, useMemo } from "react";
import { router } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { formatTimeAgo } from "@/utils/time";
@@ -15,10 +15,39 @@ interface AgentListProps {
onRefresh?: () => void;
}
interface AgentSection {
title: string;
data: AggregatedAgent[];
}
export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentListProps) {
const { theme } = useUnistyles();
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null);
// Group agents by attention status
const sections = useMemo<AgentSection[]>(() => {
const requiresAttention: AggregatedAgent[] = [];
const normal: AggregatedAgent[] = [];
for (const agent of agents) {
if (agent.requiresAttention) {
requiresAttention.push(agent);
} else {
normal.push(agent);
}
}
const result: AgentSection[] = [];
if (requiresAttention.length > 0) {
result.push({ title: "Requires Attention", data: requiresAttention });
}
if (normal.length > 0) {
result.push({ title: "All Agents", data: normal });
}
return result;
}, [agents]);
// Get the methods for the specific server
const methods = useSessionStore((state) =>
actionAgent?.serverId ? state.sessions[actionAgent.serverId]?.methods : undefined
@@ -33,6 +62,13 @@ export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentList
if (isActionSheetVisible) {
return;
}
// Clear attention flag when opening agent
const session = useSessionStore.getState().sessions[serverId];
if (session?.ws) {
session.ws.clearAgentAttention(agentId);
}
const navigationKey = buildAgentNavigationKey(serverId, agentId);
startNavigationTiming(navigationKey, {
from: "home",
@@ -66,75 +102,75 @@ export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentList
setActionAgent(null);
}, [actionAgent, deleteAgent]);
const renderAgentItem = useCallback<ListRenderItem<AggregatedAgent>>(
const renderAgentItem = useCallback<SectionListRenderItem<AggregatedAgent, AgentSection>>(
({ item: agent }) => {
const statusColor = getAgentStatusColor(agent.status);
const statusLabel = getAgentStatusLabel(agent.status);
const timeAgo = formatTimeAgo(agent.lastActivityAt);
const providerLabel = getAgentProviderDefinition(agent.provider).label;
const statusColor = getAgentStatusColor(agent.status);
const statusLabel = getAgentStatusLabel(agent.status);
const timeAgo = formatTimeAgo(agent.lastActivityAt);
const providerLabel = getAgentProviderDefinition(agent.provider).label;
return (
<Pressable
style={({ pressed }) => [
styles.agentItem,
pressed && styles.agentItemPressed,
]}
onPress={() => handleAgentPress(agent.serverId, agent.id)}
onLongPress={() => handleAgentLongPress(agent)}
>
<View style={styles.agentContent}>
<View style={styles.titleRow}>
return (
<Pressable
style={({ pressed }) => [
styles.agentItem,
pressed && styles.agentItemPressed,
]}
onPress={() => handleAgentPress(agent.serverId, agent.id)}
onLongPress={() => handleAgentLongPress(agent)}
>
<View style={styles.agentContent}>
<View style={styles.titleRow}>
<Text
style={styles.agentTitle}
numberOfLines={1}
>
{agent.title || "New Agent"}
</Text>
<View style={[styles.hostBadge, { backgroundColor: theme.colors.muted }]}>
<Text
style={styles.agentTitle}
style={[styles.hostText, { color: theme.colors.mutedForeground }]}
numberOfLines={1}
>
{agent.title || "New Agent"}
{agent.serverLabel}
</Text>
<View style={[styles.hostBadge, { backgroundColor: theme.colors.muted }]}>
</View>
</View>
<Text style={styles.agentDirectory} numberOfLines={1}>
{agent.cwd}
</Text>
<View style={styles.statusRow}>
<View style={styles.statusGroup}>
<View
style={[styles.providerBadge, { backgroundColor: theme.colors.muted }]}
>
<Text
style={[styles.hostText, { color: theme.colors.mutedForeground }]}
style={[styles.providerText, { color: theme.colors.mutedForeground }]}
numberOfLines={1}
>
{agent.serverLabel}
{providerLabel}
</Text>
</View>
<View style={styles.statusBadge}>
<View
style={[styles.statusDot, { backgroundColor: statusColor }]}
/>
<Text style={[styles.statusText, { color: statusColor }]}>
{statusLabel}
</Text>
</View>
</View>
<Text style={styles.agentDirectory} numberOfLines={1}>
{agent.cwd}
<Text style={styles.timeAgo}>
{timeAgo}
</Text>
<View style={styles.statusRow}>
<View style={styles.statusGroup}>
<View
style={[styles.providerBadge, { backgroundColor: theme.colors.muted }]}
>
<Text
style={[styles.providerText, { color: theme.colors.mutedForeground }]}
numberOfLines={1}
>
{providerLabel}
</Text>
</View>
<View style={styles.statusBadge}>
<View
style={[styles.statusDot, { backgroundColor: statusColor }]}
/>
<Text style={[styles.statusText, { color: statusColor }]}>
{statusLabel}
</Text>
</View>
</View>
<Text style={styles.timeAgo}>
{timeAgo}
</Text>
</View>
</View>
</Pressable>
);
},
</View>
</Pressable>
);
},
[handleAgentLongPress, handleAgentPress, theme.colors.muted, theme.colors.mutedForeground]
);
@@ -143,16 +179,27 @@ export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentList
[]
);
const renderSectionHeader = useCallback(
({ section }: { section: AgentSection }) => (
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>{section.title}</Text>
</View>
),
[]
);
return (
<>
<FlatList
data={agents}
<SectionList
sections={sections}
style={styles.list}
contentContainerStyle={styles.listContent}
keyExtractor={keyExtractor}
renderItem={renderAgentItem}
renderSectionHeader={renderSectionHeader}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
stickySectionHeadersEnabled={false}
refreshControl={
onRefresh ? (
<RefreshControl
@@ -219,6 +266,17 @@ const styles = StyleSheet.create((theme) => ({
paddingTop: theme.spacing[4],
paddingBottom: theme.spacing[4],
},
sectionHeader: {
paddingTop: theme.spacing[2],
paddingBottom: theme.spacing[2],
paddingHorizontal: theme.spacing[2],
},
sectionTitle: {
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.semibold,
color: theme.colors.mutedForeground,
textTransform: "uppercase",
},
agentItem: {
paddingVertical: theme.spacing[4],
paddingHorizontal: theme.spacing[4],
@@ -94,6 +94,7 @@ export function AgentStreamView({
on: () => () => {},
sendPing: () => {},
sendUserMessage: () => {},
clearAgentAttention: () => {},
}),
[]
);
@@ -334,6 +334,7 @@ function AgentFlowModal({
on: () => () => {},
sendPing: () => {},
sendUserMessage: () => {},
clearAgentAttention: () => {},
subscribeConnectionStatus: () => () => {},
getConnectionState: () => ({ isConnected: false, isConnecting: false }),
}),