fix(app): replace greedy host runtime subscriptions with targeted hooks

useHostRuntimeSession returned the full HostRuntimeSnapshot, causing
every consumer to rerender on any internal state change (probe cycles,
client generation bumps). Replace with targeted hooks that return
primitives/stable references so useSyncExternalStore skips rerenders
when the subscribed value hasn't actually changed.

New hooks: useHostRuntimeClient, useHostRuntimeConnectionStatus,
useHostRuntimeLastError, useHostRuntimeAgentDirectoryStatus,
useHostRuntimeIsDirectoryLoading. Existing useHostRuntimeIsConnected
already followed this pattern.

useHostRuntimeSnapshot kept only for settings-screen where probe
data is legitimately needed.
This commit is contained in:
Mohamed Boudra
2026-03-20 19:16:06 +07:00
parent e0f9b33d23
commit 72ea9b7a72
20 changed files with 124 additions and 70 deletions
@@ -5,7 +5,7 @@ import { useAgentCommandsQuery, type DraftCommandConfig } from './use-agent-comm
import { orderAutocompleteOptions } from '@/components/ui/autocomplete-utils'
import { useAutocomplete } from './use-autocomplete'
import { useSessionStore } from '@/stores/session-store'
import { useHostRuntimeSession } from '@/runtime/host-runtime'
import { useHostRuntimeClient, useHostRuntimeIsConnected } from '@/runtime/host-runtime'
import {
applyFileMentionReplacement,
findActiveFileMention,
@@ -138,7 +138,8 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut
return agentCwd.trim()
}, [agentCwd, isDraftContext, queryDraftConfig])
const { client, isConnected } = useHostRuntimeSession(serverId)
const client = useHostRuntimeClient(serverId)
const isConnected = useHostRuntimeIsConnected(serverId)
const mode: 'command' | 'file' | null = showFileAutocomplete
? 'file'
@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { useHostRuntimeSession } from "@/runtime/host-runtime";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import type { AgentProvider } from "@server/server/agent/agent-sdk-types";
const COMMANDS_STALE_TIME = 60_000; // Commands rarely change, cache for 1 minute
@@ -48,7 +48,8 @@ export function useAgentCommandsQuery({
enabled = true,
draftConfig,
}: UseAgentCommandsQueryOptions) {
const { client, isConnected } = useHostRuntimeSession(serverId);
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const query = useQuery({
queryKey: commandsQueryKey(serverId, agentId, draftConfig),
@@ -10,7 +10,7 @@ import type {
AgentProvider,
} from "@server/server/agent/agent-sdk-types";
import { useHosts } from "@/runtime/host-runtime";
import { useHostRuntimeSession } from "@/runtime/host-runtime";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useFormPreferences, type FormPreferences } from "./use-form-preferences";
// Explicit overrides from URL params or "New Agent" button
@@ -367,7 +367,8 @@ export function useAgentFormState(
}, [isVisible]);
// Session state for provider model listing
const { client, isConnected } = useHostRuntimeSession(formState.serverId ?? "");
const client = useHostRuntimeClient(formState.serverId ?? "");
const isConnected = useHostRuntimeIsConnected(formState.serverId ?? "");
const availableProvidersQuery = useQuery({
queryKey: ["availableProviders", formState.serverId],
@@ -3,8 +3,8 @@ import { useHosts } from "@/runtime/host-runtime";
import { useSessionStore, type Agent } from "@/stores/session-store";
import {
getHostRuntimeStore,
isHostRuntimeDirectoryLoading,
useHostRuntimeSession,
useHostRuntimeConnectionStatus,
useHostRuntimeIsDirectoryLoading,
} from "@/runtime/host-runtime";
import type {
AggregatedAgent,
@@ -88,14 +88,14 @@ export function useAllAgentsList(options?: {
const liveAgents = useSessionStore((state) =>
serverId ? state.sessions[serverId]?.agents ?? null : null
);
const { snapshot } = useHostRuntimeSession(serverId ?? "");
const connectionStatus = useHostRuntimeConnectionStatus(serverId ?? "");
const refreshAll = useCallback(() => {
if (!serverId || snapshot?.connectionStatus !== "online") {
if (!serverId || connectionStatus !== "online") {
return;
}
void runtime.refreshAgentDirectory({ serverId }).catch(() => undefined);
}, [runtime, serverId, snapshot?.connectionStatus]);
}, [runtime, serverId, connectionStatus]);
const agents = useMemo(() => {
if (!serverId || !liveAgents) {
@@ -111,7 +111,7 @@ export function useAllAgentsList(options?: {
});
}, [daemons, includeArchived, liveAgents, serverId]);
const isDirectoryLoading = Boolean(serverId && isHostRuntimeDirectoryLoading(snapshot));
const isDirectoryLoading = useHostRuntimeIsDirectoryLoading(serverId ?? "");
const isInitialLoad = isDirectoryLoading && agents.length === 0;
const isRevalidating = isDirectoryLoading && agents.length > 0;
@@ -2,7 +2,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useId, useMemo } from "react";
import { UnistylesRuntime } from "react-native-unistyles";
import { usePanelStore } from "@/stores/panel-store";
import { useHostRuntimeSession } from "@/runtime/host-runtime";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import type { SubscribeCheckoutDiffResponse } from "@server/shared/messages";
import { orderCheckoutDiffFiles } from "./checkout-diff-order";
@@ -56,7 +56,8 @@ export function useCheckoutDiffQuery({
enabled = true,
}: UseCheckoutDiffQueryOptions) {
const queryClient = useQueryClient();
const { client, isConnected } = useHostRuntimeSession(serverId);
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const mobileView = usePanelStore((state) => state.mobileView);
@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { useHostRuntimeSession } from "@/runtime/host-runtime";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import type { CheckoutPrStatusResponse } from "@server/shared/messages";
const CHECKOUT_PR_STATUS_STALE_TIME = 20_000;
@@ -21,7 +21,8 @@ export function useCheckoutPrStatusQuery({
cwd,
enabled = true,
}: UseCheckoutPrStatusQueryOptions) {
const { client, isConnected } = useHostRuntimeSession(serverId);
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const query = useQuery({
queryKey: checkoutPrStatusQueryKey(serverId, cwd),
@@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query";
import { useEffect, useMemo, useRef } from "react";
import { UnistylesRuntime } from "react-native-unistyles";
import { usePanelStore } from "@/stores/panel-store";
import { useHostRuntimeSession } from "@/runtime/host-runtime";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import type { CheckoutStatusResponse } from "@server/shared/messages";
import {
checkoutStatusRevalidationKey,
@@ -30,7 +30,8 @@ function fetchCheckoutStatus(
}
export function useCheckoutStatusQuery({ serverId, cwd }: UseCheckoutStatusQueryOptions) {
const { client, isConnected } = useHostRuntimeSession(serverId);
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const mobileView = usePanelStore((state) => state.mobileView);
@@ -87,7 +88,7 @@ export function useCheckoutStatusQuery({ serverId, cwd }: UseCheckoutStatusQuery
* only the visible agents.
*/
export function useCheckoutStatusCacheOnly({ serverId, cwd }: UseCheckoutStatusQueryOptions) {
const { client } = useHostRuntimeSession(serverId);
const client = useHostRuntimeClient(serverId);
return useQuery({
queryKey: checkoutStatusQueryKey(serverId, cwd),
+2 -2
View File
@@ -1,7 +1,7 @@
import { router } from "expo-router";
import { useCallback } from "react";
import { useToast } from "@/contexts/toast-context";
import { useHostRuntimeSession } from "@/runtime/host-runtime";
import { useHostRuntimeClient } from "@/runtime/host-runtime";
import {
normalizeWorkspaceDescriptor,
useSessionStore,
@@ -13,7 +13,7 @@ export function useOpenProject(
): (path: string) => Promise<boolean> {
const normalizedServerId = serverId?.trim() ?? "";
const toast = useToast();
const { client } = useHostRuntimeSession(normalizedServerId);
const client = useHostRuntimeClient(normalizedServerId);
const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces);
const setHasHydratedWorkspaces = useSessionStore(
(state) => state.setHasHydratedWorkspaces
@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { useHostRuntimeSession } from "@/runtime/host-runtime";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import type { ProjectIcon } from "@server/shared/messages";
export function projectIconQueryKey(serverId: string, cwd: string) {
@@ -12,7 +12,8 @@ interface UseProjectIconQueryOptions {
}
export function useProjectIconQuery({ serverId, cwd }: UseProjectIconQueryOptions) {
const { client, isConnected } = useHostRuntimeSession(serverId);
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const query = useQuery({
queryKey: projectIconQueryKey(serverId, cwd),