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:
@@ -125,6 +125,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
||||
on: () => () => {},
|
||||
sendPing: () => {},
|
||||
sendUserMessage: () => {},
|
||||
clearAgentAttention: () => {},
|
||||
subscribeConnectionStatus: () => () => {},
|
||||
getConnectionState: () => ({ isConnected: false, isConnecting: false }),
|
||||
}),
|
||||
|
||||
@@ -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 }),
|
||||
}),
|
||||
|
||||
@@ -114,6 +114,7 @@ function normalizeAgentSnapshot(snapshot: AgentSnapshotPayload, serverId: string
|
||||
const createdAt = new Date(snapshot.createdAt);
|
||||
const updatedAt = new Date(snapshot.updatedAt);
|
||||
const lastUserMessageAt = snapshot.lastUserMessageAt ? new Date(snapshot.lastUserMessageAt) : null;
|
||||
const attentionTimestamp = snapshot.attentionTimestamp ? new Date(snapshot.attentionTimestamp) : null;
|
||||
return {
|
||||
serverId,
|
||||
id: snapshot.id,
|
||||
@@ -134,6 +135,9 @@ function normalizeAgentSnapshot(snapshot: AgentSnapshotPayload, serverId: string
|
||||
title: snapshot.title ?? null,
|
||||
cwd: snapshot.cwd,
|
||||
model: snapshot.model ?? null,
|
||||
requiresAttention: snapshot.requiresAttention ?? false,
|
||||
attentionReason: snapshot.attentionReason ?? null,
|
||||
attentionTimestamp,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,9 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
|
||||
lastActivityAt: agent.lastActivityAt,
|
||||
cwd: agent.cwd,
|
||||
provider: agent.provider,
|
||||
requiresAttention: agent.requiresAttention,
|
||||
attentionReason: agent.attentionReason,
|
||||
attentionTimestamp: agent.attentionTimestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface UseWebSocketReturn {
|
||||
) => () => void;
|
||||
sendPing: () => void;
|
||||
sendUserMessage: (message: string) => void;
|
||||
clearAgentAttention: (agentId: string) => void;
|
||||
subscribeConnectionStatus?: (listener: (status: ConnectionStatusSnapshot) => void) => () => void;
|
||||
getConnectionState?: () => ConnectionStatusSnapshot;
|
||||
}
|
||||
@@ -289,6 +290,19 @@ export function useWebSocket(url: string, conversationId?: string | null): UseWe
|
||||
[send]
|
||||
);
|
||||
|
||||
const clearAgentAttention = useCallback(
|
||||
(agentId: string) => {
|
||||
send({
|
||||
type: "session",
|
||||
message: {
|
||||
type: "clear_agent_attention",
|
||||
agentId,
|
||||
},
|
||||
});
|
||||
},
|
||||
[send]
|
||||
);
|
||||
|
||||
const subscribeConnectionStatus = useCallback(
|
||||
(listener: (status: ConnectionStatusSnapshot) => void) => {
|
||||
connectionListenersRef.current.add(listener);
|
||||
@@ -318,6 +332,7 @@ export function useWebSocket(url: string, conversationId?: string | null): UseWe
|
||||
on,
|
||||
sendPing,
|
||||
sendUserMessage,
|
||||
clearAgentAttention,
|
||||
subscribeConnectionStatus,
|
||||
getConnectionState,
|
||||
}),
|
||||
@@ -330,6 +345,7 @@ export function useWebSocket(url: string, conversationId?: string | null): UseWe
|
||||
on,
|
||||
sendPing,
|
||||
sendUserMessage,
|
||||
clearAgentAttention,
|
||||
subscribeConnectionStatus,
|
||||
getConnectionState,
|
||||
]
|
||||
|
||||
@@ -94,6 +94,9 @@ export interface Agent {
|
||||
title: string | null;
|
||||
cwd: string;
|
||||
model: string | null;
|
||||
requiresAttention?: boolean;
|
||||
attentionReason?: "finished" | "error" | "permission" | null;
|
||||
attentionTimestamp?: Date | null;
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
@@ -816,6 +819,9 @@ export const useSessionStore = create<SessionStore>()(
|
||||
lastActivityAt,
|
||||
cwd: agent.cwd,
|
||||
provider: agent.provider,
|
||||
requiresAttention: agent.requiresAttention ?? false,
|
||||
attentionReason: agent.attentionReason ?? null,
|
||||
attentionTimestamp: agent.attentionTimestamp ?? null,
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
|
||||
@@ -9,4 +9,7 @@ export interface AgentDirectoryEntry {
|
||||
lastActivityAt: Date;
|
||||
cwd: string;
|
||||
provider: AgentProvider;
|
||||
requiresAttention?: boolean;
|
||||
attentionReason?: "finished" | "error" | "permission" | null;
|
||||
attentionTimestamp?: Date | null;
|
||||
}
|
||||
|
||||
@@ -819,6 +819,7 @@ export function reduceStreamUpdate(
|
||||
case "turn_failed":
|
||||
case "permission_requested":
|
||||
case "permission_resolved":
|
||||
case "attention_required":
|
||||
return finalizeActiveThoughts(state);
|
||||
default:
|
||||
return state;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -43,6 +43,13 @@ export function toStoredAgentRecord(
|
||||
lastModeId: agent.currentModeId ?? config?.modeId ?? null,
|
||||
config: config ?? null,
|
||||
persistence,
|
||||
requiresAttention: agent.attention.requiresAttention,
|
||||
attentionReason: agent.attention.requiresAttention
|
||||
? agent.attention.attentionReason
|
||||
: null,
|
||||
attentionTimestamp: agent.attention.requiresAttention
|
||||
? agent.attention.attentionTimestamp.toISOString()
|
||||
: null,
|
||||
} satisfies StoredAgentRecord;
|
||||
}
|
||||
|
||||
@@ -79,6 +86,16 @@ export function toAgentPayload(
|
||||
payload.lastError = agent.lastError;
|
||||
}
|
||||
|
||||
// Handle attention state
|
||||
payload.requiresAttention = agent.attention.requiresAttention;
|
||||
if (agent.attention.requiresAttention) {
|
||||
payload.attentionReason = agent.attention.attentionReason;
|
||||
payload.attentionTimestamp = agent.attention.attentionTimestamp.toISOString();
|
||||
} else {
|
||||
payload.attentionReason = null;
|
||||
payload.attentionTimestamp = null;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,9 @@ const STORED_AGENT_SCHEMA = z.object({
|
||||
lastModeId: z.string().nullable().optional(),
|
||||
config: SERIALIZABLE_CONFIG_SCHEMA,
|
||||
persistence: PERSISTENCE_HANDLE_SCHEMA,
|
||||
requiresAttention: z.boolean().optional(),
|
||||
attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(),
|
||||
attentionTimestamp: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export type SerializableAgentConfig = Pick<
|
||||
|
||||
@@ -80,6 +80,12 @@ export type AgentStreamEvent =
|
||||
provider: AgentProvider;
|
||||
requestId: string;
|
||||
resolution: AgentPermissionResponse;
|
||||
}
|
||||
| {
|
||||
type: "attention_required";
|
||||
provider: AgentProvider;
|
||||
reason: "finished" | "error" | "permission";
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
export type AgentPermissionRequestKind = "tool" | "plan" | "mode" | "other";
|
||||
|
||||
@@ -196,6 +196,12 @@ export const AgentStreamEventPayloadSchema = z.discriminatedUnion("type", [
|
||||
requestId: z.string(),
|
||||
resolution: AgentPermissionResponseSchema,
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("attention_required"),
|
||||
provider: AgentProviderSchema,
|
||||
reason: z.enum(["finished", "error", "permission"]),
|
||||
timestamp: z.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const AgentPersistenceHandleSchema: z.ZodType<AgentPersistenceHandle | null> =
|
||||
@@ -226,6 +232,9 @@ export const AgentSnapshotPayloadSchema = z.object({
|
||||
lastUsage: AgentUsageSchema.optional(),
|
||||
lastError: z.string().optional(),
|
||||
title: z.string().nullable(),
|
||||
requiresAttention: z.boolean().optional(),
|
||||
attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(),
|
||||
attentionTimestamp: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export type AgentSnapshotPayload = z.infer<typeof AgentSnapshotPayloadSchema>;
|
||||
@@ -448,6 +457,11 @@ export const FileExplorerRequestSchema = z.object({
|
||||
mode: z.enum(["list", "file"]),
|
||||
});
|
||||
|
||||
export const ClearAgentAttentionMessageSchema = z.object({
|
||||
type: z.literal("clear_agent_attention"),
|
||||
agentId: z.string(),
|
||||
});
|
||||
|
||||
export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
||||
UserTextMessageSchema,
|
||||
RealtimeAudioChunkMessageSchema,
|
||||
@@ -473,6 +487,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
||||
FileExplorerRequestSchema,
|
||||
ListPersistedAgentsRequestMessageSchema,
|
||||
GitRepoInfoRequestMessageSchema,
|
||||
ClearAgentAttentionMessageSchema,
|
||||
]);
|
||||
|
||||
export type SessionInboundMessage = z.infer<typeof SessionInboundMessageSchema>;
|
||||
@@ -811,6 +826,7 @@ export type GitDiffResponse = z.infer<typeof GitDiffResponseSchema>;
|
||||
export type FileExplorerRequest = z.infer<typeof FileExplorerRequestSchema>;
|
||||
export type FileExplorerResponse = z.infer<typeof FileExplorerResponseSchema>;
|
||||
export type RestartServerRequestMessage = z.infer<typeof RestartServerRequestMessageSchema>;
|
||||
export type ClearAgentAttentionMessage = z.infer<typeof ClearAgentAttentionMessageSchema>;
|
||||
|
||||
// ============================================================================
|
||||
// WebSocket Level Messages (wraps session messages)
|
||||
|
||||
@@ -837,6 +837,10 @@ export class Session {
|
||||
case "list_provider_models_request":
|
||||
await this.handleListProviderModelsRequest(msg);
|
||||
break;
|
||||
|
||||
case "clear_agent_attention":
|
||||
await this.handleClearAgentAttention(msg.agentId);
|
||||
break;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
@@ -1844,6 +1848,25 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle clearing agent attention flag
|
||||
*/
|
||||
private async handleClearAgentAttention(agentId: string): Promise<void> {
|
||||
console.log(
|
||||
`[Session ${this.clientId}] Clearing attention for agent ${agentId}`
|
||||
);
|
||||
|
||||
try {
|
||||
await this.agentManager.clearAgentAttention(agentId);
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
`[Session ${this.clientId}] Failed to clear agent attention:`,
|
||||
error
|
||||
);
|
||||
// Don't throw - this is not critical
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle agent permission response from user
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user