diff --git a/packages/app/src/app/agents.tsx b/packages/app/src/app/agents.tsx
index 3a209135a..4b803c9fe 100644
--- a/packages/app/src/app/agents.tsx
+++ b/packages/app/src/app/agents.tsx
@@ -3,10 +3,10 @@ import { View } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import { BackHeader } from "@/components/headers/back-header";
import { AgentList } from "@/components/agent-list";
-import { useAggregatedAgents } from "@/hooks/use-aggregated-agents";
+import { useAllAgentsList } from "@/hooks/use-all-agents-list";
export default function AgentsScreen() {
- const { agents, isRevalidating, refreshAll } = useAggregatedAgents();
+ const { agents, isRevalidating, refreshAll } = useAllAgentsList();
// Track user-initiated refresh to avoid showing spinner on background revalidation
const [isManualRefresh, setIsManualRefresh] = useState(false);
@@ -36,6 +36,7 @@ export default function AgentsScreen() {
diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx
index 7fe680f65..963c28fa4 100644
--- a/packages/app/src/components/agent-list.tsx
+++ b/packages/app/src/components/agent-list.tsx
@@ -31,6 +31,7 @@ import {
interface AgentListProps {
agents: AggregatedAgent[];
+ showCheckoutInfo?: boolean;
isRefreshing?: boolean;
onRefresh?: () => void;
selectedAgentId?: string;
@@ -74,6 +75,7 @@ function deriveDateSectionLabel(lastActivityAt: Date): string {
export function AgentList({
agents,
+ showCheckoutInfo = true,
isRefreshing = false,
onRefresh,
selectedAgentId,
@@ -143,8 +145,11 @@ export function AgentList({
[]
);
- const onViewableItemsChanged = useRef(
+ const onViewableItemsChanged = useCallback(
({ viewableItems }: { viewableItems: Array }) => {
+ if (!showCheckoutInfo) {
+ return;
+ }
for (const token of viewableItems) {
const agent = token.item as AggregatedAgent | undefined;
if (!agent) {
@@ -176,7 +181,8 @@ export function AgentList({
console.warn("[checkout_status] prefetch failed", error);
});
}
- }
+ },
+ [queryClient, showCheckoutInfo]
);
const AgentListRow = useCallback(
@@ -190,8 +196,10 @@ export function AgentList({
cwd: agent.cwd,
});
const checkout = checkoutQuery.data ?? null;
- const projectPath = deriveProjectPath(agent.cwd, checkout);
- const branchLabel = deriveBranchLabel(checkout);
+ const projectPath = showCheckoutInfo
+ ? deriveProjectPath(agent.cwd, checkout)
+ : agent.cwd;
+ const branchLabel = showCheckoutInfo ? deriveBranchLabel(checkout) : null;
return (
;
+ serverId: string;
+ serverLabel: string;
+}): AggregatedAgent {
+ const source = params.source;
+ return {
+ id: source.id,
+ serverId: params.serverId,
+ serverLabel: params.serverLabel,
+ title: source.title ?? null,
+ status: source.status,
+ lastActivityAt: source.lastActivityAt,
+ cwd: source.cwd,
+ provider: source.provider,
+ requiresAttention: source.requiresAttention,
+ attentionReason: source.attentionReason,
+ attentionTimestamp: source.attentionTimestamp ?? null,
+ archivedAt: source.archivedAt ?? null,
+ labels: source.labels,
+ };
+}
+
+export function useAllAgentsList(): AggregatedAgentsResult {
+ const { connectionStates } = useDaemonConnections();
+ const queryClient = useQueryClient();
+
+ const sessionClients = useSessionStore(
+ useShallow((state) => {
+ const result: Record<
+ string,
+ NonNullable | null
+ > = {};
+ for (const [serverId, session] of Object.entries(state.sessions)) {
+ result[serverId] = session.client ?? null;
+ }
+ return result;
+ })
+ );
+
+ const sessionConnections = useSessionStore(
+ useShallow((state) => {
+ const result: Record = {};
+ for (const [serverId, session] of Object.entries(state.sessions)) {
+ result[serverId] = session.connection.isConnected;
+ }
+ return result;
+ })
+ );
+
+ const liveAgents = useSessionStore(
+ useShallow((state) => {
+ const result: Record | undefined> = {};
+ for (const [serverId, session] of Object.entries(state.sessions)) {
+ result[serverId] = session.agents;
+ }
+ return result;
+ })
+ );
+
+ const serverEntries = useMemo(
+ () =>
+ Object.keys(sessionClients).map((serverId) => ({
+ serverId,
+ client: sessionClients[serverId] ?? null,
+ isConnected: sessionConnections[serverId] ?? false,
+ })),
+ [sessionClients, sessionConnections]
+ );
+
+ const queries = useQueries({
+ queries: serverEntries.map(({ serverId, client, isConnected }) => ({
+ queryKey: ["allAgents", serverId] as const,
+ queryFn: async () => {
+ if (!client) {
+ throw new Error("Daemon client not available");
+ }
+ return await client.fetchAgents();
+ },
+ enabled: Boolean(client) && isConnected,
+ staleTime: ALL_AGENTS_STALE_TIME,
+ refetchOnMount: "always" as const,
+ })),
+ });
+
+ const refreshAll = useCallback(() => {
+ for (const { serverId } of serverEntries) {
+ void queryClient.invalidateQueries({
+ queryKey: ["allAgents", serverId],
+ });
+ }
+ }, [queryClient, serverEntries]);
+
+ const agents = useMemo(() => {
+ const all: AggregatedAgent[] = [];
+
+ for (let idx = 0; idx < serverEntries.length; idx++) {
+ const entry = serverEntries[idx];
+ if (!entry) {
+ continue;
+ }
+ const data = queries[idx]?.data;
+ if (!data) {
+ continue;
+ }
+ const serverLabel =
+ connectionStates.get(entry.serverId)?.daemon.label ?? entry.serverId;
+ const liveById = liveAgents[entry.serverId];
+
+ for (const snapshot of data) {
+ const normalized = normalizeAgentSnapshot(snapshot, entry.serverId);
+ const live = liveById?.get(snapshot.id);
+ all.push(
+ toAggregatedAgent({
+ source: live ?? normalized,
+ serverId: entry.serverId,
+ serverLabel,
+ })
+ );
+ }
+ }
+
+ all.sort((left, right) => {
+ const leftRunning = left.status === "running";
+ const rightRunning = right.status === "running";
+ if (leftRunning && !rightRunning) {
+ return -1;
+ }
+ if (!leftRunning && rightRunning) {
+ return 1;
+ }
+ return right.lastActivityAt.getTime() - left.lastActivityAt.getTime();
+ });
+
+ return all;
+ }, [serverEntries, queries, connectionStates, liveAgents]);
+
+ const isFetching = queries.some((query) => query.isPending || query.isFetching);
+ const isInitialLoad = isFetching && agents.length === 0;
+ const isRevalidating = isFetching && agents.length > 0;
+
+ return {
+ agents,
+ isLoading: isFetching,
+ isInitialLoad,
+ isRevalidating,
+ refreshAll,
+ };
+}
diff --git a/packages/app/src/screens/agent/agent-ready-screen.tsx b/packages/app/src/screens/agent/agent-ready-screen.tsx
index abb9001ee..2d6576315 100644
--- a/packages/app/src/screens/agent/agent-ready-screen.tsx
+++ b/packages/app/src/screens/agent/agent-ready-screen.tsx
@@ -174,6 +174,23 @@ type AgentScreenContentProps = {
agentId?: string;
};
+type MissingAgentState =
+ | { kind: "idle" }
+ | { kind: "resolving" }
+ | { kind: "not_found"; message: string }
+ | { kind: "error"; message: string };
+
+function toErrorMessage(error: unknown): string {
+ if (error instanceof Error) {
+ return error.message;
+ }
+ return String(error);
+}
+
+function isNotFoundErrorMessage(message: string): boolean {
+ return /agent not found|not found/i.test(message);
+}
+
function AgentScreenContent({
serverId,
agentId,
@@ -363,6 +380,10 @@ function AgentScreenContent({
(state) => state.sessions[serverId]?.connection.isConnected ?? false
);
const { ensureAgentIsInitialized, refreshAgent } = useAgentInitialization(serverId);
+ const [missingAgentState, setMissingAgentState] = useState({
+ kind: "idle",
+ });
+ const initAttemptTokenRef = useRef(0);
const setFocusedAgentId = useCallback(
(agentId: string | null) => {
useSessionStore.getState().setFocusedAgentId(serverId, agentId);
@@ -544,6 +565,59 @@ function AgentScreenContent({
});
}, [resolvedAgentId, ensureAgentIsInitialized, isConnected, needsAuthoritativeSync]);
+ useEffect(() => {
+ // Clear stale resolution state when route target changes.
+ initAttemptTokenRef.current += 1;
+ setMissingAgentState({ kind: "idle" });
+ }, [serverId, resolvedAgentId]);
+
+ useEffect(() => {
+ if (!resolvedAgentId || !ensureAgentIsInitialized) {
+ return;
+ }
+ if (agent || shouldUseOptimisticStream) {
+ if (missingAgentState.kind !== "idle") {
+ setMissingAgentState({ kind: "idle" });
+ }
+ return;
+ }
+ if (!isConnected) {
+ return;
+ }
+ if (missingAgentState.kind === "resolving" || missingAgentState.kind === "not_found") {
+ return;
+ }
+
+ setMissingAgentState({ kind: "resolving" });
+ const attemptToken = ++initAttemptTokenRef.current;
+
+ ensureAgentIsInitialized(resolvedAgentId)
+ .then(() => {
+ if (attemptToken !== initAttemptTokenRef.current) {
+ return;
+ }
+ setMissingAgentState({ kind: "idle" });
+ })
+ .catch((error) => {
+ if (attemptToken !== initAttemptTokenRef.current) {
+ return;
+ }
+ const message = toErrorMessage(error);
+ if (isNotFoundErrorMessage(message)) {
+ setMissingAgentState({ kind: "not_found", message });
+ return;
+ }
+ setMissingAgentState({ kind: "error", message });
+ });
+ }, [
+ agent,
+ ensureAgentIsInitialized,
+ isConnected,
+ missingAgentState.kind,
+ resolvedAgentId,
+ shouldUseOptimisticStream,
+ ]);
+
useEffect(() => {
if (Platform.OS !== "web") {
return;
@@ -600,11 +674,28 @@ function AgentScreenContent({
);
if (!effectiveAgent) {
+ if (missingAgentState.kind === "not_found") {
+ return (
+
+
+
+ Agent not found
+
+
+ );
+ }
+
return (
-
+
- Agent not found
+
+ Loading agent…
+ {missingAgentState.kind === "error" ? (
+
+ {missingAgentState.message}
+
+ ) : null}
);
@@ -970,6 +1061,13 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.base,
color: theme.colors.foregroundMuted,
},
+ loadingSubtext: {
+ marginTop: theme.spacing[1],
+ textAlign: "center",
+ fontSize: theme.fontSize.xs,
+ color: theme.colors.foregroundMuted,
+ paddingHorizontal: theme.spacing[6],
+ },
centerState: {
flex: 1,
alignItems: "center",