Merge branch 'main' of github.com:boudra/voice-dev

This commit is contained in:
Mohamed Boudra
2025-12-20 12:57:33 +07:00
16 changed files with 305 additions and 58 deletions
@@ -125,6 +125,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
on: () => () => {}, on: () => () => {},
sendPing: () => {}, sendPing: () => {},
sendUserMessage: () => {}, sendUserMessage: () => {},
clearAgentAttention: () => {},
subscribeConnectionStatus: () => () => {}, subscribeConnectionStatus: () => () => {},
getConnectionState: () => ({ isConnected: false, isConnecting: false }), 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 { View, Text, Pressable, SectionList, Modal, RefreshControl, type SectionListRenderItem } from "react-native";
import { useCallback, useState } from "react"; import { useCallback, useState, useMemo } from "react";
import { router } from "expo-router"; import { router } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { formatTimeAgo } from "@/utils/time"; import { formatTimeAgo } from "@/utils/time";
@@ -15,10 +15,39 @@ interface AgentListProps {
onRefresh?: () => void; onRefresh?: () => void;
} }
interface AgentSection {
title: string;
data: AggregatedAgent[];
}
export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentListProps) { export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentListProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null); 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 // Get the methods for the specific server
const methods = useSessionStore((state) => const methods = useSessionStore((state) =>
actionAgent?.serverId ? state.sessions[actionAgent.serverId]?.methods : undefined actionAgent?.serverId ? state.sessions[actionAgent.serverId]?.methods : undefined
@@ -33,6 +62,13 @@ export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentList
if (isActionSheetVisible) { if (isActionSheetVisible) {
return; 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); const navigationKey = buildAgentNavigationKey(serverId, agentId);
startNavigationTiming(navigationKey, { startNavigationTiming(navigationKey, {
from: "home", from: "home",
@@ -66,75 +102,75 @@ export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentList
setActionAgent(null); setActionAgent(null);
}, [actionAgent, deleteAgent]); }, [actionAgent, deleteAgent]);
const renderAgentItem = useCallback<ListRenderItem<AggregatedAgent>>( const renderAgentItem = useCallback<SectionListRenderItem<AggregatedAgent, AgentSection>>(
({ item: agent }) => { ({ item: agent }) => {
const statusColor = getAgentStatusColor(agent.status); const statusColor = getAgentStatusColor(agent.status);
const statusLabel = getAgentStatusLabel(agent.status); const statusLabel = getAgentStatusLabel(agent.status);
const timeAgo = formatTimeAgo(agent.lastActivityAt); const timeAgo = formatTimeAgo(agent.lastActivityAt);
const providerLabel = getAgentProviderDefinition(agent.provider).label; const providerLabel = getAgentProviderDefinition(agent.provider).label;
return ( return (
<Pressable <Pressable
style={({ pressed }) => [ style={({ pressed }) => [
styles.agentItem, styles.agentItem,
pressed && styles.agentItemPressed, pressed && styles.agentItemPressed,
]} ]}
onPress={() => handleAgentPress(agent.serverId, agent.id)} onPress={() => handleAgentPress(agent.serverId, agent.id)}
onLongPress={() => handleAgentLongPress(agent)} onLongPress={() => handleAgentLongPress(agent)}
> >
<View style={styles.agentContent}> <View style={styles.agentContent}>
<View style={styles.titleRow}> <View style={styles.titleRow}>
<Text
style={styles.agentTitle}
numberOfLines={1}
>
{agent.title || "New Agent"}
</Text>
<View style={[styles.hostBadge, { backgroundColor: theme.colors.muted }]}>
<Text <Text
style={styles.agentTitle} style={[styles.hostText, { color: theme.colors.mutedForeground }]}
numberOfLines={1} numberOfLines={1}
> >
{agent.title || "New Agent"} {agent.serverLabel}
</Text> </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 <Text
style={[styles.hostText, { color: theme.colors.mutedForeground }]} style={[styles.providerText, { color: theme.colors.mutedForeground }]}
numberOfLines={1} 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> </Text>
</View> </View>
</View> </View>
<Text style={styles.agentDirectory} numberOfLines={1}> <Text style={styles.timeAgo}>
{agent.cwd} {timeAgo}
</Text> </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> </View>
</Pressable> </View>
); </Pressable>
}, );
},
[handleAgentLongPress, handleAgentPress, theme.colors.muted, theme.colors.mutedForeground] [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 ( return (
<> <>
<FlatList <SectionList
data={agents} sections={sections}
style={styles.list} style={styles.list}
contentContainerStyle={styles.listContent} contentContainerStyle={styles.listContent}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
renderItem={renderAgentItem} renderItem={renderAgentItem}
renderSectionHeader={renderSectionHeader}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
stickySectionHeadersEnabled={false}
refreshControl={ refreshControl={
onRefresh ? ( onRefresh ? (
<RefreshControl <RefreshControl
@@ -219,6 +266,17 @@ const styles = StyleSheet.create((theme) => ({
paddingTop: theme.spacing[4], paddingTop: theme.spacing[4],
paddingBottom: 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: { agentItem: {
paddingVertical: theme.spacing[4], paddingVertical: theme.spacing[4],
paddingHorizontal: theme.spacing[4], paddingHorizontal: theme.spacing[4],
@@ -94,6 +94,7 @@ export function AgentStreamView({
on: () => () => {}, on: () => () => {},
sendPing: () => {}, sendPing: () => {},
sendUserMessage: () => {}, sendUserMessage: () => {},
clearAgentAttention: () => {},
}), }),
[] []
); );
@@ -334,6 +334,7 @@ function AgentFlowModal({
on: () => () => {}, on: () => () => {},
sendPing: () => {}, sendPing: () => {},
sendUserMessage: () => {}, sendUserMessage: () => {},
clearAgentAttention: () => {},
subscribeConnectionStatus: () => () => {}, subscribeConnectionStatus: () => () => {},
getConnectionState: () => ({ isConnected: false, isConnecting: false }), getConnectionState: () => ({ isConnected: false, isConnecting: false }),
}), }),
@@ -114,6 +114,7 @@ function normalizeAgentSnapshot(snapshot: AgentSnapshotPayload, serverId: string
const createdAt = new Date(snapshot.createdAt); const createdAt = new Date(snapshot.createdAt);
const updatedAt = new Date(snapshot.updatedAt); const updatedAt = new Date(snapshot.updatedAt);
const lastUserMessageAt = snapshot.lastUserMessageAt ? new Date(snapshot.lastUserMessageAt) : null; const lastUserMessageAt = snapshot.lastUserMessageAt ? new Date(snapshot.lastUserMessageAt) : null;
const attentionTimestamp = snapshot.attentionTimestamp ? new Date(snapshot.attentionTimestamp) : null;
return { return {
serverId, serverId,
id: snapshot.id, id: snapshot.id,
@@ -134,6 +135,9 @@ function normalizeAgentSnapshot(snapshot: AgentSnapshotPayload, serverId: string
title: snapshot.title ?? null, title: snapshot.title ?? null,
cwd: snapshot.cwd, cwd: snapshot.cwd,
model: snapshot.model ?? null, model: snapshot.model ?? null,
requiresAttention: snapshot.requiresAttention ?? false,
attentionReason: snapshot.attentionReason ?? null,
attentionTimestamp,
}; };
} }
@@ -71,6 +71,9 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
lastActivityAt: agent.lastActivityAt, lastActivityAt: agent.lastActivityAt,
cwd: agent.cwd, cwd: agent.cwd,
provider: agent.provider, provider: agent.provider,
requiresAttention: agent.requiresAttention,
attentionReason: agent.attentionReason,
attentionTimestamp: agent.attentionTimestamp,
}); });
} }
} }
+16
View File
@@ -23,6 +23,7 @@ export interface UseWebSocketReturn {
) => () => void; ) => () => void;
sendPing: () => void; sendPing: () => void;
sendUserMessage: (message: string) => void; sendUserMessage: (message: string) => void;
clearAgentAttention: (agentId: string) => void;
subscribeConnectionStatus?: (listener: (status: ConnectionStatusSnapshot) => void) => () => void; subscribeConnectionStatus?: (listener: (status: ConnectionStatusSnapshot) => void) => () => void;
getConnectionState?: () => ConnectionStatusSnapshot; getConnectionState?: () => ConnectionStatusSnapshot;
} }
@@ -289,6 +290,19 @@ export function useWebSocket(url: string, conversationId?: string | null): UseWe
[send] [send]
); );
const clearAgentAttention = useCallback(
(agentId: string) => {
send({
type: "session",
message: {
type: "clear_agent_attention",
agentId,
},
});
},
[send]
);
const subscribeConnectionStatus = useCallback( const subscribeConnectionStatus = useCallback(
(listener: (status: ConnectionStatusSnapshot) => void) => { (listener: (status: ConnectionStatusSnapshot) => void) => {
connectionListenersRef.current.add(listener); connectionListenersRef.current.add(listener);
@@ -318,6 +332,7 @@ export function useWebSocket(url: string, conversationId?: string | null): UseWe
on, on,
sendPing, sendPing,
sendUserMessage, sendUserMessage,
clearAgentAttention,
subscribeConnectionStatus, subscribeConnectionStatus,
getConnectionState, getConnectionState,
}), }),
@@ -330,6 +345,7 @@ export function useWebSocket(url: string, conversationId?: string | null): UseWe
on, on,
sendPing, sendPing,
sendUserMessage, sendUserMessage,
clearAgentAttention,
subscribeConnectionStatus, subscribeConnectionStatus,
getConnectionState, getConnectionState,
] ]
+6
View File
@@ -94,6 +94,9 @@ export interface Agent {
title: string | null; title: string | null;
cwd: string; cwd: string;
model: string | null; model: string | null;
requiresAttention?: boolean;
attentionReason?: "finished" | "error" | "permission" | null;
attentionTimestamp?: Date | null;
} }
export interface Command { export interface Command {
@@ -816,6 +819,9 @@ export const useSessionStore = create<SessionStore>()(
lastActivityAt, lastActivityAt,
cwd: agent.cwd, cwd: agent.cwd,
provider: agent.provider, provider: agent.provider,
requiresAttention: agent.requiresAttention ?? false,
attentionReason: agent.attentionReason ?? null,
attentionTimestamp: agent.attentionTimestamp ?? null,
}); });
} }
return entries; return entries;
@@ -9,4 +9,7 @@ export interface AgentDirectoryEntry {
lastActivityAt: Date; lastActivityAt: Date;
cwd: string; cwd: string;
provider: AgentProvider; provider: AgentProvider;
requiresAttention?: boolean;
attentionReason?: "finished" | "error" | "permission" | null;
attentionTimestamp?: Date | null;
} }
+1
View File
@@ -821,6 +821,7 @@ export function reduceStreamUpdate(
case "turn_failed": case "turn_failed":
case "permission_requested": case "permission_requested":
case "permission_resolved": case "permission_resolved":
case "attention_required":
return finalizeActiveThoughts(state); return finalizeActiveThoughts(state);
default: default:
return state; return state;
@@ -66,6 +66,14 @@ export type WaitForAgentResult = {
lastMessage: string | null; lastMessage: string | null;
}; };
type AttentionState =
| { requiresAttention: false }
| {
requiresAttention: true;
attentionReason: "finished" | "error" | "permission";
attentionTimestamp: Date;
};
type ManagedAgentBase = { type ManagedAgentBase = {
id: string; id: string;
provider: AgentProvider; provider: AgentProvider;
@@ -84,6 +92,7 @@ type ManagedAgentBase = {
sessionId: string | null; sessionId: string | null;
lastUsage?: AgentUsage; lastUsage?: AgentUsage;
lastError?: string; lastError?: string;
attention: AttentionState;
}; };
type ManagedAgentWithSession = ManagedAgentBase & { type ManagedAgentWithSession = ManagedAgentBase & {
@@ -166,6 +175,7 @@ export class AgentManager {
private readonly maxTimelineItems: number; private readonly maxTimelineItems: number;
private readonly idFactory: () => string; private readonly idFactory: () => string;
private readonly registry?: AgentRegistry; private readonly registry?: AgentRegistry;
private readonly previousStatuses = new Map<string, AgentLifecycleStatus>();
constructor(options?: AgentManagerOptions) { constructor(options?: AgentManagerOptions) {
this.maxTimelineItems = this.maxTimelineItems =
@@ -343,6 +353,8 @@ export class AgentManager {
async closeAgent(agentId: string): Promise<void> { async closeAgent(agentId: string): Promise<void> {
const agent = this.requireAgent(agentId); const agent = this.requireAgent(agentId);
this.agents.delete(agentId); this.agents.delete(agentId);
// Clean up previousStatus to prevent memory leak
this.previousStatuses.delete(agentId);
const session = agent.session; const session = agent.session;
const closedAgent: ManagedAgent = { const closedAgent: ManagedAgent = {
...agent, ...agent,
@@ -362,6 +374,15 @@ export class AgentManager {
this.emitState(agent); 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( async runAgent(
agentId: string, agentId: string,
prompt: AgentPromptInput, prompt: AgentPromptInput,
@@ -702,9 +723,12 @@ export class AgentManager {
persistence: session.describePersistence(), persistence: session.describePersistence(),
historyPrimed: false, historyPrimed: false,
lastUserMessageAt: null, lastUserMessageAt: null,
attention: { requiresAttention: false },
} as ActiveManagedAgent; } as ActiveManagedAgent;
this.agents.set(agentId, managed); this.agents.set(agentId, managed);
// Initialize previousStatus to track transitions
this.previousStatuses.set(agentId, managed.lifecycle);
await this.persistSnapshot(managed, { await this.persistSnapshot(managed, {
title: config.title ?? null, title: config.title ?? null,
}); });
@@ -820,12 +844,76 @@ export class AgentManager {
} }
private emitState(agent: ManagedAgent): void { private emitState(agent: ManagedAgent): void {
// Check if attention should be set based on status change
this.checkAndSetAttention(agent);
this.dispatch({ this.dispatch({
type: "agent_state", type: "agent_state",
agent: { ...agent }, 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 { private dispatchStream(agentId: string, event: AgentStreamEvent): void {
this.dispatch({ type: "agent_stream", agentId, event }); this.dispatch({ type: "agent_stream", agentId, event });
} }
@@ -43,6 +43,13 @@ export function toStoredAgentRecord(
lastModeId: agent.currentModeId ?? config?.modeId ?? null, lastModeId: agent.currentModeId ?? config?.modeId ?? null,
config: config ?? null, config: config ?? null,
persistence, persistence,
requiresAttention: agent.attention.requiresAttention,
attentionReason: agent.attention.requiresAttention
? agent.attention.attentionReason
: null,
attentionTimestamp: agent.attention.requiresAttention
? agent.attention.attentionTimestamp.toISOString()
: null,
} satisfies StoredAgentRecord; } satisfies StoredAgentRecord;
} }
@@ -79,6 +86,16 @@ export function toAgentPayload(
payload.lastError = agent.lastError; 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; return payload;
} }
@@ -41,6 +41,9 @@ const STORED_AGENT_SCHEMA = z.object({
lastModeId: z.string().nullable().optional(), lastModeId: z.string().nullable().optional(),
config: SERIALIZABLE_CONFIG_SCHEMA, config: SERIALIZABLE_CONFIG_SCHEMA,
persistence: PERSISTENCE_HANDLE_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< export type SerializableAgentConfig = Pick<
@@ -80,6 +80,12 @@ export type AgentStreamEvent =
provider: AgentProvider; provider: AgentProvider;
requestId: string; requestId: string;
resolution: AgentPermissionResponse; resolution: AgentPermissionResponse;
}
| {
type: "attention_required";
provider: AgentProvider;
reason: "finished" | "error" | "permission";
timestamp: string;
}; };
export type AgentPermissionRequestKind = "tool" | "plan" | "mode" | "other"; export type AgentPermissionRequestKind = "tool" | "plan" | "mode" | "other";
+16
View File
@@ -196,6 +196,12 @@ export const AgentStreamEventPayloadSchema = z.discriminatedUnion("type", [
requestId: z.string(), requestId: z.string(),
resolution: AgentPermissionResponseSchema, 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> = const AgentPersistenceHandleSchema: z.ZodType<AgentPersistenceHandle | null> =
@@ -226,6 +232,9 @@ export const AgentSnapshotPayloadSchema = z.object({
lastUsage: AgentUsageSchema.optional(), lastUsage: AgentUsageSchema.optional(),
lastError: z.string().optional(), lastError: z.string().optional(),
title: z.string().nullable(), 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>; export type AgentSnapshotPayload = z.infer<typeof AgentSnapshotPayloadSchema>;
@@ -448,6 +457,11 @@ export const FileExplorerRequestSchema = z.object({
mode: z.enum(["list", "file"]), 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", [ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
UserTextMessageSchema, UserTextMessageSchema,
RealtimeAudioChunkMessageSchema, RealtimeAudioChunkMessageSchema,
@@ -473,6 +487,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
FileExplorerRequestSchema, FileExplorerRequestSchema,
ListPersistedAgentsRequestMessageSchema, ListPersistedAgentsRequestMessageSchema,
GitRepoInfoRequestMessageSchema, GitRepoInfoRequestMessageSchema,
ClearAgentAttentionMessageSchema,
]); ]);
export type SessionInboundMessage = z.infer<typeof SessionInboundMessageSchema>; 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 FileExplorerRequest = z.infer<typeof FileExplorerRequestSchema>;
export type FileExplorerResponse = z.infer<typeof FileExplorerResponseSchema>; export type FileExplorerResponse = z.infer<typeof FileExplorerResponseSchema>;
export type RestartServerRequestMessage = z.infer<typeof RestartServerRequestMessageSchema>; export type RestartServerRequestMessage = z.infer<typeof RestartServerRequestMessageSchema>;
export type ClearAgentAttentionMessage = z.infer<typeof ClearAgentAttentionMessageSchema>;
// ============================================================================ // ============================================================================
// WebSocket Level Messages (wraps session messages) // WebSocket Level Messages (wraps session messages)
+23
View File
@@ -837,6 +837,10 @@ export class Session {
case "list_provider_models_request": case "list_provider_models_request":
await this.handleListProviderModelsRequest(msg); await this.handleListProviderModelsRequest(msg);
break; break;
case "clear_agent_attention":
await this.handleClearAgentAttention(msg.agentId);
break;
} }
} catch (error: any) { } catch (error: any) {
console.error( 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 * Handle agent permission response from user
*/ */