refactor: rename session_state to agent_list and add subscriptions

This commit is contained in:
Mohamed Boudra
2026-01-30 13:39:40 +07:00
parent 5b32d0a56c
commit 0618dc6b0d
45 changed files with 2163 additions and 257 deletions
@@ -616,7 +616,6 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
},
listContent: {
paddingHorizontal: theme.spacing[4],
paddingTop: theme.spacing[2],
paddingBottom: theme.spacing[4],
},
@@ -626,7 +625,6 @@ const styles = StyleSheet.create((theme) => ({
justifyContent: "space-between",
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
marginHorizontal: -theme.spacing[2],
marginTop: theme.spacing[2],
borderRadius: theme.borderRadius.md,
},
@@ -643,6 +641,7 @@ const styles = StyleSheet.create((theme) => ({
backgroundColor: theme.colors.surface2,
},
sectionContainer: {
marginHorizontal: theme.spacing[2],
marginBottom: theme.spacing[2],
},
sectionDragging: {
@@ -701,7 +700,7 @@ const styles = StyleSheet.create((theme) => ({
agentItem: {
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
marginHorizontal: -theme.spacing[2],
marginLeft: theme.spacing[1],
borderRadius: theme.borderRadius.lg,
marginBottom: theme.spacing[1],
},
+12 -1
View File
@@ -78,11 +78,23 @@ function SearchInput({
autoFocus = false,
}: SearchInputProps): ReactElement {
const { theme } = useUnistyles();
const inputRef = useRef<TextInput>(null);
const InputComponent = Platform.OS === "web" ? TextInput : BottomSheetTextInput;
useEffect(() => {
if (autoFocus && IS_WEB && inputRef.current) {
const timer = setTimeout(() => {
inputRef.current?.focus();
}, 50);
return () => clearTimeout(timer);
}
}, [autoFocus]);
return (
<View style={styles.searchInputContainer}>
<Search size={16} color={theme.colors.foregroundMuted} />
<InputComponent
ref={inputRef as any}
// @ts-expect-error - outlineStyle is web-only
style={[styles.searchInput, IS_WEB && { outlineStyle: "none" }]}
placeholder={placeholder}
@@ -91,7 +103,6 @@ function SearchInput({
onChangeText={onChangeText}
autoCapitalize="none"
autoCorrect={false}
autoFocus={autoFocus}
onSubmitEditing={onSubmitEditing}
/>
</View>
@@ -3,18 +3,18 @@ import type { ReactNode } from "react";
import { useDaemonRegistry, type DaemonProfile } from "./daemon-registry-context";
export type ConnectionState =
| { status: "idle"; lastError: null; lastOnlineAt: string | null; sessionReady: false; hasEverReceivedSessionState: false }
| { status: "connecting"; lastError: null; lastOnlineAt: string | null; sessionReady: false; hasEverReceivedSessionState: boolean }
| { status: "online"; lastError: null; lastOnlineAt: string; sessionReady: boolean; hasEverReceivedSessionState: boolean }
| { status: "offline"; lastError: string | null; lastOnlineAt: string | null; sessionReady: false; hasEverReceivedSessionState: boolean }
| { status: "error"; lastError: string; lastOnlineAt: string | null; sessionReady: false; hasEverReceivedSessionState: boolean };
| { status: "idle"; lastError: null; lastOnlineAt: string | null; agentListReady: false; hasEverReceivedAgentList: false }
| { status: "connecting"; lastError: null; lastOnlineAt: string | null; agentListReady: false; hasEverReceivedAgentList: boolean }
| { status: "online"; lastError: null; lastOnlineAt: string; agentListReady: boolean; hasEverReceivedAgentList: boolean }
| { status: "offline"; lastError: string | null; lastOnlineAt: string | null; agentListReady: false; hasEverReceivedAgentList: boolean }
| { status: "error"; lastError: string; lastOnlineAt: string | null; agentListReady: false; hasEverReceivedAgentList: boolean };
export type ConnectionStatus = ConnectionState["status"];
type ConnectionStateUpdate =
| { status: "idle" }
| { status: "connecting"; lastOnlineAt?: string | null }
| { status: "online"; lastOnlineAt: string; sessionReady?: boolean }
| { status: "online"; lastOnlineAt: string; agentListReady?: boolean }
| { status: "offline"; lastError?: string | null; lastOnlineAt?: string | null }
| { status: "error"; lastError: string; lastOnlineAt?: string | null };
@@ -35,8 +35,8 @@ function createDefaultConnectionState(): ConnectionState {
status: "idle",
lastError: null,
lastOnlineAt: null,
sessionReady: false,
hasEverReceivedSessionState: false,
agentListReady: false,
hasEverReceivedAgentList: false,
};
}
@@ -50,43 +50,43 @@ function resolveNextConnectionState(
status: "idle",
lastError: null,
lastOnlineAt: existing.lastOnlineAt,
sessionReady: false,
hasEverReceivedSessionState: false,
agentListReady: false,
hasEverReceivedAgentList: false,
};
case "connecting":
return {
status: "connecting",
lastError: null,
lastOnlineAt: update.lastOnlineAt ?? existing.lastOnlineAt,
sessionReady: false,
hasEverReceivedSessionState: existing.hasEverReceivedSessionState ?? false,
agentListReady: false,
hasEverReceivedAgentList: existing.hasEverReceivedAgentList ?? false,
};
case "online":
const currentSessionReady = update.sessionReady ?? (existing.status === "online" ? existing.sessionReady : false);
const currentAgentListReady = update.agentListReady ?? (existing.status === "online" ? existing.agentListReady : false);
return {
status: "online",
lastError: null,
lastOnlineAt: update.lastOnlineAt,
sessionReady: currentSessionReady,
hasEverReceivedSessionState:
currentSessionReady || existing.hasEverReceivedSessionState || false,
agentListReady: currentAgentListReady,
hasEverReceivedAgentList:
currentAgentListReady || existing.hasEverReceivedAgentList || false,
};
case "offline":
return {
status: "offline",
lastError: update.lastError ?? null,
lastOnlineAt: update.lastOnlineAt ?? existing.lastOnlineAt,
sessionReady: false,
hasEverReceivedSessionState: existing.hasEverReceivedSessionState ?? false,
agentListReady: false,
hasEverReceivedAgentList: existing.hasEverReceivedAgentList ?? false,
};
case "error":
return {
status: "error",
lastError: update.lastError,
lastOnlineAt: update.lastOnlineAt ?? existing.lastOnlineAt,
sessionReady: false,
hasEverReceivedSessionState: existing.hasEverReceivedSessionState ?? false,
agentListReady: false,
hasEverReceivedAgentList: existing.hasEverReceivedAgentList ?? false,
};
}
}
+83 -24
View File
@@ -247,6 +247,7 @@ function normalizeAgentSnapshot(
attentionReason: snapshot.attentionReason ?? null,
attentionTimestamp,
archivedAt,
labels: snapshot.labels,
};
}
@@ -411,6 +412,7 @@ export function SessionProvider({
| null
>(null);
const hasRequestedInitialSnapshotRef = useRef(false);
const agentUpdatesSubscriptionIdRef = useRef<string | null>(null);
const sessionStateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
null
);
@@ -674,6 +676,15 @@ export function SessionProvider({
useEffect(() => {
if (!connectionSnapshot.isConnected) {
hasRequestedInitialSnapshotRef.current = false;
const subscriptionId = agentUpdatesSubscriptionIdRef.current;
if (subscriptionId && client) {
try {
client.unsubscribeAgentUpdates(subscriptionId);
} catch {
// no-op
}
}
agentUpdatesSubscriptionIdRef.current = null;
return;
}
if (hasRequestedInitialSnapshotRef.current) {
@@ -687,15 +698,21 @@ export function SessionProvider({
let retryCount = 0;
const requestSessionState = () => {
const requestAgentList = () => {
console.log(
`[Session] Requesting session_state (attempt ${retryCount + 1}/${
`[Session] Requesting agent_list (attempt ${retryCount + 1}/${
MAX_RETRIES + 1
})`,
{ serverId }
);
void client
.requestSessionState();
.requestAgentList({ filter: { labels: { ui: "true" } } });
if (!agentUpdatesSubscriptionIdRef.current) {
agentUpdatesSubscriptionIdRef.current = client.subscribeAgentUpdates({
subscriptionId: `app:${serverId}`,
filter: { labels: { ui: "true" } },
});
}
if (sessionStateTimeoutRef.current) {
clearTimeout(sessionStateTimeoutRef.current);
@@ -705,7 +722,7 @@ export function SessionProvider({
if (retryCount < MAX_RETRIES) {
retryCount++;
console.warn(
`[Session] session_state timeout, retrying in ${RETRY_DELAY_MS}ms`,
`[Session] agent_list timeout, retrying in ${RETRY_DELAY_MS}ms`,
{
serverId,
attempt: retryCount,
@@ -714,11 +731,11 @@ export function SessionProvider({
);
setTimeout(() => {
requestSessionState();
requestAgentList();
}, RETRY_DELAY_MS);
} else {
console.error(
`[Session] session_state failed after ${MAX_RETRIES} retries`,
`[Session] agent_list failed after ${MAX_RETRIES} retries`,
{ serverId }
);
@@ -726,13 +743,13 @@ export function SessionProvider({
updateConnectionStatus(serverId, {
status: "online",
lastOnlineAt: new Date().toISOString(),
sessionReady: true,
agentListReady: true,
});
}
}, TIMEOUT_MS);
};
requestSessionState();
requestAgentList();
return () => {
if (sessionStateTimeoutRef.current) {
@@ -744,10 +761,10 @@ export function SessionProvider({
// Daemon message handlers - directly update Zustand store
useEffect(() => {
console.log("[Session] Setting up session_state listener for", serverId);
console.log("[Session] Setting up agent_list listener for", serverId);
const unsubSessionState = client.on("session_state", (message) => {
if (message.type !== "session_state") return;
const unsubAgentList = client.on("agent_list", (message) => {
if (message.type !== "agent_list") return;
if (sessionStateTimeoutRef.current) {
clearTimeout(sessionStateTimeoutRef.current);
@@ -757,7 +774,7 @@ export function SessionProvider({
const { agents: agentsList } = message.payload;
console.log(
"[Session] ✅ Received session_state:",
"[Session] ✅ Received agent_list:",
agentsList.length,
"agents"
);
@@ -849,16 +866,57 @@ export function SessionProvider({
updateConnectionStatus(serverId, {
status: "online",
lastOnlineAt: new Date().toISOString(),
sessionReady: true,
agentListReady: true,
});
});
const unsubAgentState = client.on("agent_state", (message) => {
if (message.type !== "agent_state") return;
const snapshot = message.payload;
const agent = normalizeAgentSnapshot(snapshot, serverId);
const unsubAgentUpdate = client.on("agent_update", (message) => {
if (message.type !== "agent_update") return;
const update = message.payload;
console.log("[Session] Agent state update:", agent.id, agent.status);
if (update.kind === "remove") {
const agentId = update.agentId;
previousAgentStatusRef.current.delete(agentId);
setAgents(serverId, (prev) => {
if (!prev.has(agentId)) {
return prev;
}
const next = new Map(prev);
next.delete(agentId);
return next;
});
setPendingPermissions(serverId, (prev) => {
if (prev.size === 0) {
return prev;
}
let changed = false;
const next = new Map(prev);
for (const [key, pending] of Array.from(next.entries())) {
if (pending.agentId === agentId) {
next.delete(key);
changed = true;
}
}
return changed ? next : prev;
});
setQueuedMessages(serverId, (prev) => {
if (!prev.has(agentId)) {
return prev;
}
const next = new Map(prev);
next.delete(agentId);
return next;
});
return;
}
const agent = normalizeAgentSnapshot(update.agent, serverId);
console.log("[Session] Agent update:", agent.id, agent.status);
setAgents(serverId, (prev) => {
const next = new Map(prev);
@@ -967,7 +1025,7 @@ export function SessionProvider({
});
// NOTE: We don't update lastActivityAt on every stream event to prevent
// cascading rerenders. The agent_state handler updates agent.lastActivityAt
// cascading rerenders. The agent_update handler updates agent.lastActivityAt
// on status changes, which is sufficient for sorting and display purposes.
});
@@ -1462,8 +1520,8 @@ export function SessionProvider({
});
return () => {
unsubSessionState();
unsubAgentState();
unsubAgentList();
unsubAgentUpdate();
unsubAgentStream();
unsubAgentStreamSnapshot();
unsubStatus();
@@ -1834,7 +1892,7 @@ export function SessionProvider({
[encodeImages, serverId, client, setAgentStreamTail]
);
// Keep the ref updated so the agent_state handler can call it
// Keep the ref updated so the agent_update handler can call it
sendAgentMessageRef.current = sendAgentMessage;
const cancelAgentRun = useCallback(
@@ -1934,6 +1992,7 @@ export function SessionProvider({
}
return client.createAgent({
config,
labels: { ui: "true" },
...(trimmedPrompt ? { initialPrompt: trimmedPrompt } : {}),
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),
...(git ? { git } : {}),
@@ -2166,9 +2225,9 @@ export function SessionProvider({
return;
}
try {
client.requestSessionState();
client.requestAgentList({ filter: { labels: { ui: "true" } } });
} catch (error: any) {
console.error("[Session] Failed to refresh session:", error);
console.error("[Session] Failed to refresh agent list:", error);
}
}, [client, serverId]);
+11 -10
View File
@@ -75,6 +75,7 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
attentionReason: agent.attentionReason,
attentionTimestamp: agent.attentionTimestamp,
archivedAt: agent.archivedAt,
labels: agent.labels,
};
allAgents.push(nextAgent);
}
@@ -103,23 +104,23 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
const isConnecting = Array.from(connectionStates.entries()).some(([id, c]) => {
const shortId = id.substring(0, 20);
// First-time connection (never received session state)
if (c.status === 'connecting' && !c.hasEverReceivedSessionState) {
// First-time connection (never received agent list)
if (c.status === 'connecting' && !c.hasEverReceivedAgentList) {
connectingReasons.push(`${shortId}: first-time connecting`);
return true;
}
if (c.status === 'online' && !c.hasEverReceivedSessionState) {
connectingReasons.push(`${shortId}: online but no session_state yet`);
if (c.status === 'online' && !c.hasEverReceivedAgentList) {
connectingReasons.push(`${shortId}: online but no agent_list yet`);
return true;
}
// Reconnecting (have received session state before)
if (c.status === 'connecting' && c.hasEverReceivedSessionState) {
// Reconnecting (have received agent list before)
if (c.status === 'connecting' && c.hasEverReceivedAgentList) {
connectingReasons.push(`${shortId}: reconnecting`);
return true;
}
if (c.status === 'online' && !c.sessionReady && c.hasEverReceivedSessionState) {
connectingReasons.push(`${shortId}: online but sessionReady=false (waiting for session_state)`);
if (c.status === 'online' && !c.agentListReady && c.hasEverReceivedAgentList) {
connectingReasons.push(`${shortId}: online but agentListReady=false (waiting for agent_list)`);
return true;
}
@@ -139,8 +140,8 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
const connectionStatesArray = Array.from(connectionStates.entries()).map(([id, state]) => ({
id: id.substring(0, 20) + (id.length > 20 ? '...' : ''),
status: state.status,
sessionReady: state.sessionReady,
hasEverReceivedSessionState: state.hasEverReceivedSessionState,
agentListReady: state.agentListReady,
hasEverReceivedAgentList: state.hasEverReceivedAgentList,
}));
console.log('[useAggregatedAgents] States:', {
@@ -497,6 +497,7 @@ function AgentScreenContent({
title: "Agent",
cwd: ".",
model: null,
labels: {},
};
}, [resolvedAgentId, serverId, shouldUseOptimisticStream]);
@@ -273,13 +273,20 @@ export function DraftAgentScreen({
if (!selectedServerId || !sessionAgents) {
return [];
}
const uniquePaths = new Set<string>();
const pathLastCreated = new Map<string, Date>();
sessionAgents.forEach((agent) => {
if (agent.cwd) {
uniquePaths.add(agent.cwd);
if (agent.cwd && !agent.cwd.includes(".paseo/worktrees")) {
const existing = pathLastCreated.get(agent.cwd);
if (!existing || agent.createdAt > existing) {
pathLastCreated.set(agent.cwd, agent.createdAt);
}
}
});
return Array.from(uniquePaths).sort();
return Array.from(pathLastCreated.keys()).sort((a, b) => {
const aTime = pathLastCreated.get(a)!.getTime();
const bTime = pathLastCreated.get(b)!.getTime();
return bTime - aTime;
});
}, [selectedServerId, sessionAgents]);
const sessionClient = useSessionStore((state) =>
@@ -600,6 +607,7 @@ export function DraftAgentScreen({
title: "New agent",
cwd,
model,
labels: {},
};
}, [
machine.tag,
+2
View File
@@ -99,6 +99,7 @@ export interface Agent {
attentionReason?: "finished" | "error" | "permission" | null;
attentionTimestamp?: Date | null;
archivedAt?: Date | null;
labels: Record<string, string>;
}
export type ExplorerEntryKind = "file" | "directory";
@@ -838,6 +839,7 @@ export const useSessionStore = create<SessionStore>()(
requiresAttention: agent.requiresAttention ?? false,
attentionReason: agent.attentionReason ?? null,
attentionTimestamp: agent.attentionTimestamp ?? null,
labels: agent.labels,
});
}
return entries;
+16 -15
View File
@@ -1,16 +1,17 @@
import type { AgentLifecycleStatus } from "@server/server/agent/agent-manager";
import type { AgentProvider } from "@server/server/agent/agent-sdk-types";
import type { Agent } from "@/stores/session-store";
export interface AgentDirectoryEntry {
id: string;
serverId: string;
title: string | null;
status: AgentLifecycleStatus;
lastActivityAt: Date;
cwd: string;
provider: AgentProvider;
requiresAttention?: boolean;
attentionReason?: "finished" | "error" | "permission" | null;
attentionTimestamp?: Date | null;
archivedAt?: Date | null;
}
export type AgentDirectoryEntry = Pick<
Agent,
| "id"
| "serverId"
| "title"
| "status"
| "lastActivityAt"
| "cwd"
| "provider"
| "requiresAttention"
| "attentionReason"
| "attentionTimestamp"
| "archivedAt"
| "labels"
>;