feat: extract lightweight agent directory (Phase 1)
This commit is contained in:
@@ -33,6 +33,7 @@ import * as FileSystem from 'expo-file-system';
|
|||||||
import { useDaemonConnections } from "./daemon-connections-context";
|
import { useDaemonConnections } from "./daemon-connections-context";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { getNowMs, isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
|
import { getNowMs, isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
|
||||||
|
import type { AgentDirectoryEntry } from "@/types/agent-directory";
|
||||||
|
|
||||||
const SESSION_CONTEXT_LOG_TAG = "[SessionContext]";
|
const SESSION_CONTEXT_LOG_TAG = "[SessionContext]";
|
||||||
|
|
||||||
@@ -298,6 +299,22 @@ function buildSessionStateFromSnapshots(serverId: string, snapshots: AgentSnapsh
|
|||||||
return { agents, pendingPermissions };
|
return { agents, pendingPermissions };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildAgentDirectoryEntries(serverId: string, agents: Map<string, Agent>): AgentDirectoryEntry[] {
|
||||||
|
const entries: AgentDirectoryEntry[] = [];
|
||||||
|
for (const agent of agents.values()) {
|
||||||
|
entries.push({
|
||||||
|
id: agent.id,
|
||||||
|
serverId,
|
||||||
|
title: agent.title ?? null,
|
||||||
|
status: agent.status,
|
||||||
|
lastActivityAt: agent.lastActivityAt,
|
||||||
|
cwd: agent.cwd,
|
||||||
|
provider: agent.provider,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface SessionContextValue {
|
export interface SessionContextValue {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
@@ -400,6 +417,8 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
const setSession = useSessionStore((state) => state.setSession);
|
const setSession = useSessionStore((state) => state.setSession);
|
||||||
const updateSession = useSessionStore((state) => state.updateSession);
|
const updateSession = useSessionStore((state) => state.updateSession);
|
||||||
const clearSession = useSessionStore((state) => state.clearSession);
|
const clearSession = useSessionStore((state) => state.clearSession);
|
||||||
|
const setAgentDirectory = useSessionStore((state) => state.setAgentDirectory);
|
||||||
|
const clearAgentDirectory = useSessionStore((state) => state.clearAgentDirectory);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ws.isConnected) {
|
if (ws.isConnected) {
|
||||||
@@ -455,6 +474,13 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
const [hasHydratedAgents, setHasHydratedAgents] = useState(false);
|
const [hasHydratedAgents, setHasHydratedAgents] = useState(false);
|
||||||
|
|
||||||
const [agents, setAgents] = useState<Map<string, Agent>>(new Map());
|
const [agents, setAgents] = useState<Map<string, Agent>>(new Map());
|
||||||
|
const lightweightAgentDirectory = useMemo(
|
||||||
|
() => buildAgentDirectoryEntries(serverId, agents),
|
||||||
|
[agents, serverId]
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
setAgentDirectory(serverId, lightweightAgentDirectory);
|
||||||
|
}, [lightweightAgentDirectory, serverId, setAgentDirectory]);
|
||||||
const [commands, setCommands] = useState<Map<string, Command>>(new Map());
|
const [commands, setCommands] = useState<Map<string, Command>>(new Map());
|
||||||
const [pendingPermissions, setPendingPermissions] = useState<Map<string, PendingPermission>>(new Map());
|
const [pendingPermissions, setPendingPermissions] = useState<Map<string, PendingPermission>>(new Map());
|
||||||
const [gitDiffs, setGitDiffs] = useState<Map<string, string>>(new Map());
|
const [gitDiffs, setGitDiffs] = useState<Map<string, string>>(new Map());
|
||||||
@@ -1892,8 +1918,9 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
clearSession(serverId);
|
clearSession(serverId);
|
||||||
|
clearAgentDirectory(serverId);
|
||||||
};
|
};
|
||||||
}, [clearSession, serverId]);
|
}, [clearAgentDirectory, clearSession, serverId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SessionContext.Provider value={value}>
|
<SessionContext.Provider value={value}>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { Agent } from "@/contexts/session-context";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
|
import type { AgentDirectoryEntry } from "@/types/agent-directory";
|
||||||
|
|
||||||
export interface AggregatedAgent extends Agent {
|
export interface AggregatedAgent extends AgentDirectoryEntry {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
serverLabel: string;
|
serverLabel: string;
|
||||||
}
|
}
|
||||||
@@ -15,26 +15,17 @@ export interface AggregatedAgentsResult {
|
|||||||
|
|
||||||
export function useAggregatedAgents(): AggregatedAgentsResult {
|
export function useAggregatedAgents(): AggregatedAgentsResult {
|
||||||
const { connectionStates } = useDaemonConnections();
|
const { connectionStates } = useDaemonConnections();
|
||||||
const sessions = useSessionStore((state) => state.sessions);
|
const agentDirectory = useSessionStore((state) => state.agentDirectory);
|
||||||
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(() => {
|
return useMemo(() => {
|
||||||
const allAgents: AggregatedAgent[] = [];
|
const allAgents: AggregatedAgent[] = [];
|
||||||
|
|
||||||
for (const [serverId, agents] of Object.entries(sessionAgents)) {
|
for (const [serverId, agents] of Object.entries(agentDirectory)) {
|
||||||
if (!agents || agents.size === 0) {
|
if (!agents || agents.length === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId;
|
const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId;
|
||||||
for (const agent of agents.values()) {
|
for (const agent of agents) {
|
||||||
allAgents.push({
|
allAgents.push({
|
||||||
...agent,
|
...agent,
|
||||||
serverId,
|
serverId,
|
||||||
@@ -53,8 +44,8 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
|
|||||||
if (!leftRunning && rightRunning) {
|
if (!leftRunning && rightRunning) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const leftTime = (left.lastUserMessageAt ?? left.lastActivityAt).getTime();
|
const leftTime = left.lastActivityAt.getTime();
|
||||||
const rightTime = (right.lastUserMessageAt ?? right.lastActivityAt).getTime();
|
const rightTime = right.lastActivityAt.getTime();
|
||||||
return rightTime - leftTime;
|
return rightTime - leftTime;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,5 +61,5 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return { agents: allAgents, isLoading };
|
return { agents: allAgents, isLoading };
|
||||||
}, [sessionAgents, connectionStates]);
|
}, [agentDirectory, connectionStates]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useSyncExternalStore } from "react";
|
import { useSyncExternalStore } from "react";
|
||||||
import type { SessionContextValue } from "@/contexts/session-context";
|
import type { SessionContextValue } from "@/contexts/session-context";
|
||||||
|
import type { AgentDirectoryEntry } from "@/types/agent-directory";
|
||||||
import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
|
import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
|
||||||
|
|
||||||
// SessionData mirrors SessionContextValue so consumers can subscribe to a single source of truth.
|
// SessionData mirrors SessionContextValue so consumers can subscribe to a single source of truth.
|
||||||
@@ -7,6 +8,7 @@ export type SessionData = SessionContextValue;
|
|||||||
|
|
||||||
interface SessionStoreState {
|
interface SessionStoreState {
|
||||||
sessions: Record<string, SessionData>;
|
sessions: Record<string, SessionData>;
|
||||||
|
agentDirectory: Record<string, AgentDirectoryEntry[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SessionStore extends SessionStoreState {
|
interface SessionStore extends SessionStoreState {
|
||||||
@@ -14,11 +16,14 @@ interface SessionStore extends SessionStoreState {
|
|||||||
updateSession: (serverId: string, partial: Partial<SessionData>) => void;
|
updateSession: (serverId: string, partial: Partial<SessionData>) => void;
|
||||||
clearSession: (serverId: string) => void;
|
clearSession: (serverId: string) => void;
|
||||||
getSession: (serverId: string) => SessionData | undefined;
|
getSession: (serverId: string) => SessionData | undefined;
|
||||||
|
setAgentDirectory: (serverId: string, agents: AgentDirectoryEntry[]) => void;
|
||||||
|
clearAgentDirectory: (serverId: string) => void;
|
||||||
|
getAgentDirectory: (serverId: string) => AgentDirectoryEntry[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionListener = () => void;
|
type SessionListener = () => void;
|
||||||
|
|
||||||
let storeState: SessionStoreState = { sessions: {} };
|
let storeState: SessionStoreState = { sessions: {}, agentDirectory: {} };
|
||||||
const listeners = new Set<SessionListener>();
|
const listeners = new Set<SessionListener>();
|
||||||
const SESSION_STORE_LOG_TAG = "[SessionStore]";
|
const SESSION_STORE_LOG_TAG = "[SessionStore]";
|
||||||
let sessionStoreUpdateCount = 0;
|
let sessionStoreUpdateCount = 0;
|
||||||
@@ -30,7 +35,7 @@ const emit = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const logSessionStoreUpdate = (
|
const logSessionStoreUpdate = (
|
||||||
type: "setSession" | "updateSession" | "clearSession",
|
type: "setSession" | "updateSession" | "clearSession" | "setAgentDirectory" | "clearAgentDirectory",
|
||||||
serverId: string,
|
serverId: string,
|
||||||
payload?: unknown
|
payload?: unknown
|
||||||
) => {
|
) => {
|
||||||
@@ -65,6 +70,7 @@ const setSession: SessionStore["setSession"] = (serverId, data) => {
|
|||||||
}
|
}
|
||||||
logSessionStoreUpdate("setSession", serverId, data);
|
logSessionStoreUpdate("setSession", serverId, data);
|
||||||
return {
|
return {
|
||||||
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
...prev.sessions,
|
...prev.sessions,
|
||||||
[serverId]: data,
|
[serverId]: data,
|
||||||
@@ -92,6 +98,7 @@ const updateSession: SessionStore["updateSession"] = (serverId, partial) => {
|
|||||||
|
|
||||||
logSessionStoreUpdate("updateSession", serverId, partial);
|
logSessionStoreUpdate("updateSession", serverId, partial);
|
||||||
return {
|
return {
|
||||||
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
...prev.sessions,
|
...prev.sessions,
|
||||||
[serverId]: next,
|
[serverId]: next,
|
||||||
@@ -108,7 +115,7 @@ const clearSession: SessionStore["clearSession"] = (serverId) => {
|
|||||||
logSessionStoreUpdate("clearSession", serverId);
|
logSessionStoreUpdate("clearSession", serverId);
|
||||||
const nextSessions = { ...prev.sessions };
|
const nextSessions = { ...prev.sessions };
|
||||||
delete nextSessions[serverId];
|
delete nextSessions[serverId];
|
||||||
return { sessions: nextSessions };
|
return { ...prev, sessions: nextSessions };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -116,6 +123,39 @@ const getSession: SessionStore["getSession"] = (serverId) => {
|
|||||||
return storeState.sessions[serverId];
|
return storeState.sessions[serverId];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setAgentDirectory: SessionStore["setAgentDirectory"] = (serverId, agents) => {
|
||||||
|
updateStoreState((prev) => {
|
||||||
|
const existing = prev.agentDirectory[serverId];
|
||||||
|
if (existing && areAgentDirectoriesEqual(existing, agents)) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
logSessionStoreUpdate("setAgentDirectory", serverId, { agentCount: agents.length });
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
agentDirectory: {
|
||||||
|
...prev.agentDirectory,
|
||||||
|
[serverId]: agents,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearAgentDirectory: SessionStore["clearAgentDirectory"] = (serverId) => {
|
||||||
|
updateStoreState((prev) => {
|
||||||
|
if (!(serverId in prev.agentDirectory)) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
logSessionStoreUpdate("clearAgentDirectory", serverId);
|
||||||
|
const nextDirectory = { ...prev.agentDirectory };
|
||||||
|
delete nextDirectory[serverId];
|
||||||
|
return { ...prev, agentDirectory: nextDirectory };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAgentDirectory: SessionStore["getAgentDirectory"] = (serverId) => {
|
||||||
|
return storeState.agentDirectory[serverId];
|
||||||
|
};
|
||||||
|
|
||||||
const subscribe = (listener: SessionListener) => {
|
const subscribe = (listener: SessionListener) => {
|
||||||
listeners.add(listener);
|
listeners.add(listener);
|
||||||
return () => {
|
return () => {
|
||||||
@@ -125,10 +165,14 @@ const subscribe = (listener: SessionListener) => {
|
|||||||
|
|
||||||
const buildSnapshot = (): SessionStore => ({
|
const buildSnapshot = (): SessionStore => ({
|
||||||
sessions: storeState.sessions,
|
sessions: storeState.sessions,
|
||||||
|
agentDirectory: storeState.agentDirectory,
|
||||||
setSession,
|
setSession,
|
||||||
updateSession,
|
updateSession,
|
||||||
clearSession,
|
clearSession,
|
||||||
getSession,
|
getSession,
|
||||||
|
setAgentDirectory,
|
||||||
|
clearAgentDirectory,
|
||||||
|
getAgentDirectory,
|
||||||
});
|
});
|
||||||
|
|
||||||
const shallowEqual = (left: SessionData, right: SessionData): boolean => {
|
const shallowEqual = (left: SessionData, right: SessionData): boolean => {
|
||||||
@@ -155,3 +199,33 @@ export function useSessionStore<T>(selector: (state: SessionStore) => T): T {
|
|||||||
() => selector(buildSnapshot())
|
() => selector(buildSnapshot())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function areAgentDirectoriesEqual(
|
||||||
|
left: AgentDirectoryEntry[] | undefined,
|
||||||
|
right: AgentDirectoryEntry[]
|
||||||
|
): boolean {
|
||||||
|
if (!left) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (left.length !== right.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const leftById = new Map(left.map((entry) => [entry.id, entry]));
|
||||||
|
for (const entry of right) {
|
||||||
|
const previous = leftById.get(entry.id);
|
||||||
|
if (!previous) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
previous.serverId !== entry.serverId ||
|
||||||
|
previous.title !== entry.title ||
|
||||||
|
previous.status !== entry.status ||
|
||||||
|
previous.provider !== entry.provider ||
|
||||||
|
previous.cwd !== entry.cwd ||
|
||||||
|
previous.lastActivityAt.getTime() !== entry.lastActivityAt.getTime()
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { AgentLifecycleStatus } from "@server/server/agent/agent-manager";
|
||||||
|
import type { AgentProvider } from "@server/server/agent/agent-sdk-types";
|
||||||
|
|
||||||
|
export interface AgentDirectoryEntry {
|
||||||
|
id: string;
|
||||||
|
serverId: string;
|
||||||
|
title: string | null;
|
||||||
|
status: AgentLifecycleStatus;
|
||||||
|
lastActivityAt: Date;
|
||||||
|
cwd: string;
|
||||||
|
provider: AgentProvider;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user