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
@@ -66,6 +66,14 @@ export type WaitForAgentResult = {
lastMessage: string | null;
};
type AttentionState =
| { requiresAttention: false }
| {
requiresAttention: true;
attentionReason: "finished" | "error" | "permission";
attentionTimestamp: Date;
};
type ManagedAgentBase = {
id: string;
provider: AgentProvider;
@@ -84,6 +92,7 @@ type ManagedAgentBase = {
sessionId: string | null;
lastUsage?: AgentUsage;
lastError?: string;
attention: AttentionState;
};
type ManagedAgentWithSession = ManagedAgentBase & {
@@ -166,6 +175,7 @@ export class AgentManager {
private readonly maxTimelineItems: number;
private readonly idFactory: () => string;
private readonly registry?: AgentRegistry;
private readonly previousStatuses = new Map<string, AgentLifecycleStatus>();
constructor(options?: AgentManagerOptions) {
this.maxTimelineItems =
@@ -343,6 +353,8 @@ export class AgentManager {
async closeAgent(agentId: string): Promise<void> {
const agent = this.requireAgent(agentId);
this.agents.delete(agentId);
// Clean up previousStatus to prevent memory leak
this.previousStatuses.delete(agentId);
const session = agent.session;
const closedAgent: ManagedAgent = {
...agent,
@@ -362,6 +374,15 @@ export class AgentManager {
this.emitState(agent);
}
async clearAgentAttention(agentId: string): Promise<void> {
const agent = this.requireAgent(agentId);
if (agent.attention.requiresAttention) {
agent.attention = { requiresAttention: false };
await this.persistSnapshot(agent);
this.emitState(agent);
}
}
async runAgent(
agentId: string,
prompt: AgentPromptInput,
@@ -702,9 +723,12 @@ export class AgentManager {
persistence: session.describePersistence(),
historyPrimed: false,
lastUserMessageAt: null,
attention: { requiresAttention: false },
} as ActiveManagedAgent;
this.agents.set(agentId, managed);
// Initialize previousStatus to track transitions
this.previousStatuses.set(agentId, managed.lifecycle);
await this.persistSnapshot(managed, {
title: config.title ?? null,
});
@@ -820,12 +844,76 @@ export class AgentManager {
}
private emitState(agent: ManagedAgent): void {
// Check if attention should be set based on status change
this.checkAndSetAttention(agent);
this.dispatch({
type: "agent_state",
agent: { ...agent },
});
}
private checkAndSetAttention(agent: ManagedAgent): void {
const previousStatus = this.previousStatuses.get(agent.id);
const currentStatus = agent.lifecycle;
// Track the new status
this.previousStatuses.set(agent.id, currentStatus);
// Skip if already requires attention
if (agent.attention.requiresAttention) {
return;
}
// Check if agent transitioned from running to idle (finished)
if (previousStatus === "running" && currentStatus === "idle") {
agent.attention = {
requiresAttention: true,
attentionReason: "finished",
attentionTimestamp: new Date(),
};
this.broadcastAgentAttention(agent, "finished");
void this.persistSnapshot(agent);
return;
}
// Check if agent entered error state
if (currentStatus === "error") {
agent.attention = {
requiresAttention: true,
attentionReason: "error",
attentionTimestamp: new Date(),
};
this.broadcastAgentAttention(agent, "error");
void this.persistSnapshot(agent);
return;
}
// Check if agent has pending permissions
if (agent.pendingPermissions.size > 0) {
agent.attention = {
requiresAttention: true,
attentionReason: "permission",
attentionTimestamp: new Date(),
};
this.broadcastAgentAttention(agent, "permission");
void this.persistSnapshot(agent);
return;
}
}
private broadcastAgentAttention(
agent: ManagedAgent,
reason: "finished" | "error" | "permission"
): void {
this.dispatchStream(agent.id, {
type: "attention_required",
provider: agent.provider,
reason,
timestamp: new Date().toISOString(),
});
}
private dispatchStream(agentId: string, event: AgentStreamEvent): void {
this.dispatch({ type: "agent_stream", agentId, event });
}