refactor(app): simplify session directory with direct snapshots and add sessionReady state

Replace subscription-based session accessor pattern with direct snapshot updates in daemon connections context. Add sessionReady flag to connection state to track when initial agent list has been received. Auto-select first ready host in create agent modal. Migrate from app.json to app.config.js. Add android:release script and Playwright testing docs.
This commit is contained in:
Mohamed Boudra
2025-11-27 21:06:48 +01:00
parent a01c3f9b73
commit bf58e81ce8
12 changed files with 339 additions and 277 deletions
+18 -21
View File
@@ -1,7 +1,6 @@
import { useMemo } from "react";
import type { Agent } from "@/contexts/session-context";
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import { useSessionDirectory } from "@/hooks/use-session-directory";
export interface AggregatedAgent extends Agent {
serverId: string;
@@ -14,25 +13,24 @@ export interface AggregatedAgentsResult {
}
export function useAggregatedAgents(): AggregatedAgentsResult {
const { connectionStates, isLoading: registryLoading } = useDaemonConnections();
const sessionDirectory = useSessionDirectory();
const { connectionStates, sessionSnapshots } = useDaemonConnections();
return useMemo(() => {
const allAgents: AggregatedAgent[] = [];
sessionDirectory.forEach((session, serverId) => {
if (!session) {
return;
for (const [serverId, snapshot] of sessionSnapshots) {
if (snapshot.agents.size === 0) {
continue;
}
const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId;
for (const agent of session.agents.values()) {
for (const agent of snapshot.agents.values()) {
allAgents.push({
...agent,
serverId,
serverLabel,
});
}
});
}
// Sort by: running agents first, then by most recent activity
allAgents.sort((left, right) => {
@@ -49,19 +47,18 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
return rightTime - leftTime;
});
// Loading if registry is still loading, or if any host is connecting and hasn't
// reported agents yet (idle hosts that haven't connected don't block loading)
let isLoading = registryLoading;
if (!isLoading && connectionStates.size > 0) {
for (const [serverId, record] of connectionStates) {
const hasSession = sessionDirectory.has(serverId);
if (record.status === "connecting" && !hasSession) {
isLoading = true;
break;
}
}
}
const isLoading = Array.from(connectionStates.values()).some(c => {
// Only these states count as "loading":
// 1. Actively connecting
// 2. Online but session not yet received
if (c.status === 'connecting') return true;
if (c.status === 'online' && !c.sessionReady) return true;
// Offline/error = not loading, just unavailable (normal state)
return false;
});
console.log("[useAggregatedAgents] aggregated agent count:", allAgents.length, "isLoading:", isLoading);
return { agents: allAgents, isLoading };
}, [sessionDirectory, connectionStates, registryLoading]);
}, [sessionSnapshots, connectionStates]);
}
+1 -1
View File
@@ -16,7 +16,7 @@ export class DaemonSessionUnavailableError extends Error {
export function getSessionForServer(
serverId: string,
directory: Map<string, SessionContextValue | null>
directory: Map<string, SessionContextValue>
): SessionContextValue {
const session = directory.get(serverId) ?? null;
if (!session) {
@@ -1,35 +1,20 @@
import { useEffect, useMemo, useState } from "react";
import { useMemo } from "react";
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import type { SessionContextValue } from "@/contexts/session-context";
export function useSessionDirectory(): Map<string, SessionContextValue | null> {
const { sessionAccessors, subscribeToSessionDirectory } = useDaemonConnections();
const [revision, setRevision] = useState(0);
useEffect(() => {
return subscribeToSessionDirectory(() => {
setRevision((current) => current + 1);
});
}, [subscribeToSessionDirectory]);
export function useSessionDirectory(): Map<string, SessionContextValue> {
const { sessionSnapshots } = useDaemonConnections();
return useMemo(() => {
const entries = new Map<string, SessionContextValue | null>();
sessionAccessors.forEach((entry, serverId) => {
try {
entries.set(serverId, entry.getSnapshot());
} catch (error) {
console.error(`[useSessionDirectory] Failed to read session accessor for "${serverId}"`, error);
entries.set(serverId, null);
}
const entries = new Map<string, SessionContextValue>();
sessionSnapshots.forEach((snapshot, serverId) => {
entries.set(serverId, snapshot);
});
return entries;
}, [sessionAccessors, revision]);
}, [sessionSnapshots]);
}
export function useSessionForServer(serverId: string | null): SessionContextValue | null {
const directory = useSessionDirectory();
if (!serverId) {
return null;
}
return directory.get(serverId) ?? null;
const { sessionSnapshots } = useDaemonConnections();
return serverId ? sessionSnapshots.get(serverId) ?? null : null;
}
+4 -1
View File
@@ -207,7 +207,10 @@ export function useWebSocket(url: string, conversationId?: string | null): UseWe
}, []);
const on = useCallback(
(type: SessionOutboundMessage["type"], handler: (message: SessionOutboundMessage) => void) => {
(
type: SessionOutboundMessage["type"],
handler: (message: SessionOutboundMessage) => void
) => {
if (!handlersRef.current.has(type)) {
handlersRef.current.set(type, new Set());
}