Merge branch 'main' of github.com:boudra/voice-dev
This commit is contained in:
@@ -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 }),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user