feat(app): add zustand store for session state management
This commit is contained in:
@@ -60,7 +60,8 @@
|
||||
"react-native-unistyles": "^3.0.15",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.5.1",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.23.8",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "~19.2.0",
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { useDaemonRegistry, type DaemonProfile } from "./daemon-registry-context";
|
||||
import type { SessionContextValue } from "./session-context";
|
||||
|
||||
export type SessionSnapshot = SessionContextValue;
|
||||
|
||||
export type ConnectionState =
|
||||
| { status: "idle"; lastError: null; lastOnlineAt: string | null; sessionReady: false }
|
||||
@@ -29,9 +26,6 @@ interface DaemonConnectionsContextValue {
|
||||
connectionStates: Map<string, DaemonConnectionRecord>;
|
||||
isLoading: boolean;
|
||||
updateConnectionStatus: (daemonId: string, update: ConnectionStateUpdate) => void;
|
||||
sessionSnapshots: Map<string, SessionSnapshot>;
|
||||
updateSessionSnapshot: (daemonId: string, snapshot: SessionSnapshot | null) => void;
|
||||
clearSessionSnapshot: (daemonId: string) => void;
|
||||
}
|
||||
|
||||
const DaemonConnectionsContext = createContext<DaemonConnectionsContextValue | null>(null);
|
||||
@@ -116,7 +110,6 @@ export function useDaemonConnections(): DaemonConnectionsContextValue {
|
||||
export function DaemonConnectionsProvider({ children }: { children: ReactNode }) {
|
||||
const { daemons, isLoading: registryLoading } = useDaemonRegistry();
|
||||
const [connectionStates, setConnectionStates] = useState<Map<string, DaemonConnectionRecord>>(new Map());
|
||||
const [sessionSnapshotRegistry, setSessionSnapshotRegistry] = useState<Map<string, SessionSnapshot>>(new Map());
|
||||
|
||||
// Ensure connection states stay in sync with registry entries
|
||||
useEffect(() => {
|
||||
@@ -133,24 +126,6 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode })
|
||||
});
|
||||
}, [daemons]);
|
||||
|
||||
useEffect(() => {
|
||||
setSessionSnapshotRegistry((prev) => {
|
||||
const validDaemonIds = new Set(daemons.map((daemon) => daemon.id));
|
||||
let changed = false;
|
||||
const next = new Map<string, SessionSnapshot>();
|
||||
|
||||
for (const [daemonId, snapshot] of prev.entries()) {
|
||||
if (!validDaemonIds.has(daemonId)) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
next.set(daemonId, snapshot);
|
||||
}
|
||||
|
||||
return changed ? next : prev;
|
||||
});
|
||||
}, [daemons]);
|
||||
|
||||
|
||||
const updateConnectionStatus = useCallback(
|
||||
(daemonId: string, update: ConnectionStateUpdate) => {
|
||||
@@ -177,38 +152,10 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode })
|
||||
[]
|
||||
);
|
||||
|
||||
const updateSessionSnapshot = useCallback((serverId: string, snapshot: SessionSnapshot | null) => {
|
||||
setSessionSnapshotRegistry((prev) => {
|
||||
const next = new Map(prev);
|
||||
if (snapshot === null) {
|
||||
next.delete(serverId);
|
||||
} else {
|
||||
next.set(serverId, snapshot);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const clearSessionSnapshot = useCallback((serverId: string) => {
|
||||
setSessionSnapshotRegistry((prev) => {
|
||||
if (!prev.has(serverId)) {
|
||||
return prev;
|
||||
}
|
||||
const next = new Map(prev);
|
||||
next.delete(serverId);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const sessionSnapshots = useMemo(() => new Map(sessionSnapshotRegistry), [sessionSnapshotRegistry]);
|
||||
|
||||
const value: DaemonConnectionsContextValue = {
|
||||
connectionStates,
|
||||
isLoading: registryLoading,
|
||||
updateConnectionStatus,
|
||||
sessionSnapshots,
|
||||
updateSessionSnapshot,
|
||||
clearSessionSnapshot,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
import { ScrollView } from "react-native";
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import { useDaemonConnections } from "./daemon-connections-context";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
|
||||
const derivePendingPermissionKey = (agentId: string, request: AgentPermissionRequest) => {
|
||||
const fallbackId =
|
||||
@@ -396,9 +397,10 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
||||
const wsIsConnected = ws.isConnected;
|
||||
const {
|
||||
updateConnectionStatus,
|
||||
updateSessionSnapshot,
|
||||
clearSessionSnapshot,
|
||||
} = useDaemonConnections();
|
||||
const setSession = useSessionStore((state) => state.setSession);
|
||||
const updateSession = useSessionStore((state) => state.updateSession);
|
||||
const clearSession = useSessionStore((state) => state.clearSession);
|
||||
|
||||
useEffect(() => {
|
||||
if (ws.isConnected) {
|
||||
@@ -1843,21 +1845,19 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateSessionSnapshot(serverId, value);
|
||||
}, [serverId, updateSessionSnapshot, value]);
|
||||
setSession(serverId, { ...value });
|
||||
}, [serverId, setSession]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wsIsConnected) {
|
||||
return;
|
||||
}
|
||||
clearSessionSnapshot(serverId);
|
||||
}, [clearSessionSnapshot, serverId, wsIsConnected]);
|
||||
const payload = { ...value };
|
||||
updateSession(serverId, payload);
|
||||
}, [serverId, updateSession, value]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearSessionSnapshot(serverId);
|
||||
clearSession(serverId);
|
||||
};
|
||||
}, [serverId, clearSessionSnapshot]);
|
||||
}, [clearSession, serverId]);
|
||||
|
||||
return (
|
||||
<SessionContext.Provider value={value}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from "react";
|
||||
import type { Agent } from "@/contexts/session-context";
|
||||
import { useMemo } from "react";
|
||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
|
||||
export interface AggregatedAgent extends Agent {
|
||||
serverId: string;
|
||||
@@ -13,17 +14,27 @@ export interface AggregatedAgentsResult {
|
||||
}
|
||||
|
||||
export function useAggregatedAgents(): AggregatedAgentsResult {
|
||||
const { connectionStates, sessionSnapshots } = useDaemonConnections();
|
||||
const { connectionStates } = useDaemonConnections();
|
||||
const sessions = useSessionStore((state) => state.sessions);
|
||||
const sessionAgents = useMemo(() => {
|
||||
const agentsByServer: Record<string, Map<string, Agent>> = {};
|
||||
for (const [serverId, session] of Object.entries(sessions)) {
|
||||
if (session?.agents) {
|
||||
agentsByServer[serverId] = session.agents;
|
||||
}
|
||||
}
|
||||
return agentsByServer;
|
||||
}, [sessions]);
|
||||
|
||||
return useMemo(() => {
|
||||
const allAgents: AggregatedAgent[] = [];
|
||||
|
||||
for (const [serverId, snapshot] of sessionSnapshots) {
|
||||
if (snapshot.agents.size === 0) {
|
||||
for (const [serverId, agents] of Object.entries(sessionAgents)) {
|
||||
if (!agents || agents.size === 0) {
|
||||
continue;
|
||||
}
|
||||
const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId;
|
||||
for (const agent of snapshot.agents.values()) {
|
||||
for (const agent of agents.values()) {
|
||||
allAgents.push({
|
||||
...agent,
|
||||
serverId,
|
||||
@@ -58,7 +69,6 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
|
||||
return false;
|
||||
});
|
||||
|
||||
console.log("[useAggregatedAgents] aggregated agent count:", allAgents.length, "isLoading:", isLoading);
|
||||
return { agents: allAgents, isLoading };
|
||||
}, [sessionSnapshots, connectionStates]);
|
||||
}, [sessionAgents, connectionStates]);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import { useMemo } from "react";
|
||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||
import type { SessionContextValue } from "@/contexts/session-context";
|
||||
import { useSessionStore, type SessionData } from "@/stores/session-store";
|
||||
|
||||
export function useSessionDirectory(): Map<string, SessionContextValue> {
|
||||
const { sessionSnapshots } = useDaemonConnections();
|
||||
export function useSessionDirectory(): Map<string, SessionData> {
|
||||
const sessions = useSessionStore((state) => state.sessions);
|
||||
|
||||
return useMemo(() => {
|
||||
const entries = new Map<string, SessionContextValue>();
|
||||
sessionSnapshots.forEach((snapshot, serverId) => {
|
||||
entries.set(serverId, snapshot);
|
||||
});
|
||||
return entries;
|
||||
}, [sessionSnapshots]);
|
||||
return new Map<string, SessionData>(Object.entries(sessions));
|
||||
}, [sessions]);
|
||||
}
|
||||
|
||||
export function useSessionForServer(serverId: string | null): SessionContextValue | null {
|
||||
const { sessionSnapshots } = useDaemonConnections();
|
||||
return serverId ? sessionSnapshots.get(serverId) ?? null : null;
|
||||
export function useSessionForServer(serverId: string | null): SessionData | null {
|
||||
return useSessionStore((state) => (serverId ? state.sessions[serverId] ?? null : null));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { create } from "zustand";
|
||||
import type { SessionContextValue } from "@/contexts/session-context";
|
||||
|
||||
// SessionData mirrors SessionContextValue so consumers can subscribe to a single source of truth.
|
||||
export type SessionData = SessionContextValue;
|
||||
|
||||
interface SessionStore {
|
||||
sessions: Record<string, SessionData>;
|
||||
setSession: (serverId: string, data: SessionData) => void;
|
||||
updateSession: (serverId: string, partial: Partial<SessionData>) => void;
|
||||
clearSession: (serverId: string) => void;
|
||||
getSession: (serverId: string) => SessionData | undefined;
|
||||
}
|
||||
|
||||
export const useSessionStore = create<SessionStore>((set, get) => ({
|
||||
sessions: {},
|
||||
setSession: (serverId, data) => {
|
||||
set((state) => ({
|
||||
sessions: {
|
||||
...state.sessions,
|
||||
[serverId]: data,
|
||||
},
|
||||
}));
|
||||
},
|
||||
updateSession: (serverId, partial) => {
|
||||
set((state) => {
|
||||
const existing = state.sessions[serverId];
|
||||
const next: SessionData | undefined = existing
|
||||
? { ...existing, ...partial, serverId }
|
||||
: (partial.serverId ? { ...(partial as SessionData), serverId } : undefined);
|
||||
|
||||
if (!next) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
sessions: {
|
||||
...state.sessions,
|
||||
[serverId]: next,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
clearSession: (serverId) => {
|
||||
set((state) => {
|
||||
if (!(serverId in state.sessions)) {
|
||||
return state;
|
||||
}
|
||||
const nextSessions = { ...state.sessions };
|
||||
delete nextSessions[serverId];
|
||||
return { sessions: nextSessions };
|
||||
});
|
||||
},
|
||||
getSession: (serverId) => get().sessions[serverId],
|
||||
}));
|
||||
Reference in New Issue
Block a user