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
+2 -2
View File
@@ -23,7 +23,7 @@ import {
getHostRuntimeStore, getHostRuntimeStore,
useHosts, useHosts,
useHostMutations, useHostMutations,
useHostRuntimeSession, useHostRuntimeClient,
} from "@/runtime/host-runtime"; } from "@/runtime/host-runtime";
import { SessionProvider } from "@/contexts/session-context"; import { SessionProvider } from "@/contexts/session-context";
import type { HostProfile } from "@/types/host-connection"; import type { HostProfile } from "@/types/host-connection";
@@ -176,7 +176,7 @@ function PushNotificationRouter() {
} }
function ManagedDaemonSession({ daemon }: { daemon: HostProfile }) { function ManagedDaemonSession({ daemon }: { daemon: HostProfile }) {
const { client } = useHostRuntimeSession(daemon.serverId); const client = useHostRuntimeClient(daemon.serverId);
if (!client) { if (!client) {
return null; return null;
@@ -1,7 +1,7 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useLocalSearchParams, useRouter } from "expo-router"; import { useLocalSearchParams, useRouter } from "expo-router";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import { useHostRuntimeSession } from "@/runtime/host-runtime"; import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { import {
buildHostRootRoute, buildHostRootRoute,
buildHostWorkspaceAgentRoute, buildHostWorkspaceAgentRoute,
@@ -16,7 +16,8 @@ export default function HostAgentReadyRoute() {
const redirectedRef = useRef(false); const redirectedRef = useRef(false);
const serverId = typeof params.serverId === "string" ? params.serverId : ""; const serverId = typeof params.serverId === "string" ? params.serverId : "";
const agentId = typeof params.agentId === "string" ? params.agentId : ""; const agentId = typeof params.agentId === "string" ? params.agentId : "";
const { client, isConnected } = useHostRuntimeSession(serverId); const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const agentCwd = useSessionStore((state) => { const agentCwd = useSessionStore((state) => {
if (!serverId || !agentId) { if (!serverId || !agentId) {
return null; return null;
@@ -28,7 +28,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
import { Shortcut } from '@/components/ui/shortcut' import { Shortcut } from '@/components/ui/shortcut'
import { Autocomplete } from '@/components/ui/autocomplete' import { Autocomplete } from '@/components/ui/autocomplete'
import { useAgentAutocomplete } from '@/hooks/use-agent-autocomplete' import { useAgentAutocomplete } from '@/hooks/use-agent-autocomplete'
import { useHostRuntimeSession } from '@/runtime/host-runtime' import { useHostRuntimeAgentDirectoryStatus, useHostRuntimeClient, useHostRuntimeIsConnected } from '@/runtime/host-runtime'
import { import {
deleteAttachments, deleteAttachments,
persistAttachmentFromBlob, persistAttachmentFromBlob,
@@ -100,14 +100,16 @@ export function AgentInputArea({
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const isScreenFocused = useIsFocused() const isScreenFocused = useIsFocused()
const { client, isConnected, snapshot } = useHostRuntimeSession(serverId) const client = useHostRuntimeClient(serverId)
const isConnected = useHostRuntimeIsConnected(serverId)
const agentDirectoryStatus = useHostRuntimeAgentDirectoryStatus(serverId)
const toast = useToast() const toast = useToast()
const voice = useVoiceOptional() const voice = useVoiceOptional()
const isDictationReady = const isDictationReady =
isConnected && isConnected &&
(snapshot?.agentDirectoryStatus === 'ready' || (agentDirectoryStatus === 'ready' ||
snapshot?.agentDirectoryStatus === 'revalidating' || agentDirectoryStatus === 'revalidating' ||
snapshot?.agentDirectoryStatus === 'error_after_ready') agentDirectoryStatus === 'error_after_ready')
const agent = useSessionStore((state) => state.sessions[serverId]?.agents?.get(agentId)) const agent = useSessionStore((state) => state.sessions[serverId]?.agents?.get(agentId))
@@ -15,7 +15,7 @@ import { usePathname } from "expo-router";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { shortenPath } from "@/utils/shorten-path"; import { shortenPath } from "@/utils/shorten-path";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import { useHosts, useHostRuntimeSession } from "@/runtime/host-runtime"; import { useHosts, useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useOpenProject } from "@/hooks/use-open-project"; import { useOpenProject } from "@/hooks/use-open-project";
import { parseServerIdFromPathname } from "@/utils/host-routes"; import { parseServerIdFromPathname } from "@/utils/host-routes";
import { buildWorkingDirectorySuggestions } from "@/utils/working-directory-suggestions"; import { buildWorkingDirectorySuggestions } from "@/utils/working-directory-suggestions";
@@ -34,7 +34,8 @@ export function ProjectPickerModal() {
return daemons[0]?.serverId ?? null; return daemons[0]?.serverId ?? null;
}, [pathname, daemons]); }, [pathname, daemons]);
const { client, isConnected } = useHostRuntimeSession(serverId ?? ""); const client = useHostRuntimeClient(serverId ?? "");
const isConnected = useHostRuntimeIsConnected(serverId ?? "");
const workspaces = useSessionStore((state) => const workspaces = useSessionStore((state) =>
serverId ? state.sessions[serverId]?.workspaces : undefined serverId ? state.sessions[serverId]?.workspaces : undefined
); );
@@ -19,7 +19,7 @@ import Svg, {
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import type { ListTerminalsResponse } from "@server/shared/messages"; import type { ListTerminalsResponse } from "@server/shared/messages";
import { encodeTerminalKeyInput } from "@server/shared/terminal-key-input"; import { encodeTerminalKeyInput } from "@server/shared/terminal-key-input";
import { useHostRuntimeSession } from "@/runtime/host-runtime"; import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
import { import {
hasPendingTerminalModifiers, hasPendingTerminalModifiers,
@@ -150,7 +150,8 @@ export function TerminalPane({
}); });
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { client, isConnected } = useHostRuntimeSession(serverId); const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const scopeKey = useMemo(() => terminalScopeKey({ serverId, cwd }), [serverId, cwd]); const scopeKey = useMemo(() => terminalScopeKey({ serverId, cwd }), [serverId, cwd]);
const terminalsQueryKey = useMemo(() => ["terminals", serverId, cwd] as const, [cwd, serverId]); const terminalsQueryKey = useMemo(() => ["terminals", serverId, cwd] as const, [cwd, serverId]);
@@ -5,7 +5,7 @@ import { useAgentCommandsQuery, type DraftCommandConfig } from './use-agent-comm
import { orderAutocompleteOptions } from '@/components/ui/autocomplete-utils' import { orderAutocompleteOptions } from '@/components/ui/autocomplete-utils'
import { useAutocomplete } from './use-autocomplete' import { useAutocomplete } from './use-autocomplete'
import { useSessionStore } from '@/stores/session-store' import { useSessionStore } from '@/stores/session-store'
import { useHostRuntimeSession } from '@/runtime/host-runtime' import { useHostRuntimeClient, useHostRuntimeIsConnected } from '@/runtime/host-runtime'
import { import {
applyFileMentionReplacement, applyFileMentionReplacement,
findActiveFileMention, findActiveFileMention,
@@ -138,7 +138,8 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut
return agentCwd.trim() return agentCwd.trim()
}, [agentCwd, isDraftContext, queryDraftConfig]) }, [agentCwd, isDraftContext, queryDraftConfig])
const { client, isConnected } = useHostRuntimeSession(serverId) const client = useHostRuntimeClient(serverId)
const isConnected = useHostRuntimeIsConnected(serverId)
const mode: 'command' | 'file' | null = showFileAutocomplete const mode: 'command' | 'file' | null = showFileAutocomplete
? 'file' ? 'file'
@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query"; 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"; import type { AgentProvider } from "@server/server/agent/agent-sdk-types";
const COMMANDS_STALE_TIME = 60_000; // Commands rarely change, cache for 1 minute const COMMANDS_STALE_TIME = 60_000; // Commands rarely change, cache for 1 minute
@@ -48,7 +48,8 @@ export function useAgentCommandsQuery({
enabled = true, enabled = true,
draftConfig, draftConfig,
}: UseAgentCommandsQueryOptions) { }: UseAgentCommandsQueryOptions) {
const { client, isConnected } = useHostRuntimeSession(serverId); const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const query = useQuery({ const query = useQuery({
queryKey: commandsQueryKey(serverId, agentId, draftConfig), queryKey: commandsQueryKey(serverId, agentId, draftConfig),
@@ -10,7 +10,7 @@ import type {
AgentProvider, AgentProvider,
} from "@server/server/agent/agent-sdk-types"; } from "@server/server/agent/agent-sdk-types";
import { useHosts } from "@/runtime/host-runtime"; 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"; import { useFormPreferences, type FormPreferences } from "./use-form-preferences";
// Explicit overrides from URL params or "New Agent" button // Explicit overrides from URL params or "New Agent" button
@@ -367,7 +367,8 @@ export function useAgentFormState(
}, [isVisible]); }, [isVisible]);
// Session state for provider model listing // 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({ const availableProvidersQuery = useQuery({
queryKey: ["availableProviders", formState.serverId], queryKey: ["availableProviders", formState.serverId],
@@ -3,8 +3,8 @@ import { useHosts } from "@/runtime/host-runtime";
import { useSessionStore, type Agent } from "@/stores/session-store"; import { useSessionStore, type Agent } from "@/stores/session-store";
import { import {
getHostRuntimeStore, getHostRuntimeStore,
isHostRuntimeDirectoryLoading, useHostRuntimeConnectionStatus,
useHostRuntimeSession, useHostRuntimeIsDirectoryLoading,
} from "@/runtime/host-runtime"; } from "@/runtime/host-runtime";
import type { import type {
AggregatedAgent, AggregatedAgent,
@@ -88,14 +88,14 @@ export function useAllAgentsList(options?: {
const liveAgents = useSessionStore((state) => const liveAgents = useSessionStore((state) =>
serverId ? state.sessions[serverId]?.agents ?? null : null serverId ? state.sessions[serverId]?.agents ?? null : null
); );
const { snapshot } = useHostRuntimeSession(serverId ?? ""); const connectionStatus = useHostRuntimeConnectionStatus(serverId ?? "");
const refreshAll = useCallback(() => { const refreshAll = useCallback(() => {
if (!serverId || snapshot?.connectionStatus !== "online") { if (!serverId || connectionStatus !== "online") {
return; return;
} }
void runtime.refreshAgentDirectory({ serverId }).catch(() => undefined); void runtime.refreshAgentDirectory({ serverId }).catch(() => undefined);
}, [runtime, serverId, snapshot?.connectionStatus]); }, [runtime, serverId, connectionStatus]);
const agents = useMemo(() => { const agents = useMemo(() => {
if (!serverId || !liveAgents) { if (!serverId || !liveAgents) {
@@ -111,7 +111,7 @@ export function useAllAgentsList(options?: {
}); });
}, [daemons, includeArchived, liveAgents, serverId]); }, [daemons, includeArchived, liveAgents, serverId]);
const isDirectoryLoading = Boolean(serverId && isHostRuntimeDirectoryLoading(snapshot)); const isDirectoryLoading = useHostRuntimeIsDirectoryLoading(serverId ?? "");
const isInitialLoad = isDirectoryLoading && agents.length === 0; const isInitialLoad = isDirectoryLoading && agents.length === 0;
const isRevalidating = 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 { useCallback, useEffect, useId, useMemo } from "react";
import { UnistylesRuntime } from "react-native-unistyles"; import { UnistylesRuntime } from "react-native-unistyles";
import { usePanelStore } from "@/stores/panel-store"; 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 type { SubscribeCheckoutDiffResponse } from "@server/shared/messages";
import { orderCheckoutDiffFiles } from "./checkout-diff-order"; import { orderCheckoutDiffFiles } from "./checkout-diff-order";
@@ -56,7 +56,8 @@ export function useCheckoutDiffQuery({
enabled = true, enabled = true,
}: UseCheckoutDiffQueryOptions) { }: UseCheckoutDiffQueryOptions) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { client, isConnected } = useHostRuntimeSession(serverId); const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const isMobile = const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const mobileView = usePanelStore((state) => state.mobileView); const mobileView = usePanelStore((state) => state.mobileView);
@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query"; 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"; import type { CheckoutPrStatusResponse } from "@server/shared/messages";
const CHECKOUT_PR_STATUS_STALE_TIME = 20_000; const CHECKOUT_PR_STATUS_STALE_TIME = 20_000;
@@ -21,7 +21,8 @@ export function useCheckoutPrStatusQuery({
cwd, cwd,
enabled = true, enabled = true,
}: UseCheckoutPrStatusQueryOptions) { }: UseCheckoutPrStatusQueryOptions) {
const { client, isConnected } = useHostRuntimeSession(serverId); const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const query = useQuery({ const query = useQuery({
queryKey: checkoutPrStatusQueryKey(serverId, cwd), queryKey: checkoutPrStatusQueryKey(serverId, cwd),
@@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query";
import { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef } from "react";
import { UnistylesRuntime } from "react-native-unistyles"; import { UnistylesRuntime } from "react-native-unistyles";
import { usePanelStore } from "@/stores/panel-store"; 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 type { CheckoutStatusResponse } from "@server/shared/messages";
import { import {
checkoutStatusRevalidationKey, checkoutStatusRevalidationKey,
@@ -30,7 +30,8 @@ function fetchCheckoutStatus(
} }
export function useCheckoutStatusQuery({ serverId, cwd }: UseCheckoutStatusQueryOptions) { export function useCheckoutStatusQuery({ serverId, cwd }: UseCheckoutStatusQueryOptions) {
const { client, isConnected } = useHostRuntimeSession(serverId); const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const isMobile = const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const mobileView = usePanelStore((state) => state.mobileView); const mobileView = usePanelStore((state) => state.mobileView);
@@ -87,7 +88,7 @@ export function useCheckoutStatusQuery({ serverId, cwd }: UseCheckoutStatusQuery
* only the visible agents. * only the visible agents.
*/ */
export function useCheckoutStatusCacheOnly({ serverId, cwd }: UseCheckoutStatusQueryOptions) { export function useCheckoutStatusCacheOnly({ serverId, cwd }: UseCheckoutStatusQueryOptions) {
const { client } = useHostRuntimeSession(serverId); const client = useHostRuntimeClient(serverId);
return useQuery({ return useQuery({
queryKey: checkoutStatusQueryKey(serverId, cwd), queryKey: checkoutStatusQueryKey(serverId, cwd),
+2 -2
View File
@@ -1,7 +1,7 @@
import { router } from "expo-router"; import { router } from "expo-router";
import { useCallback } from "react"; import { useCallback } from "react";
import { useToast } from "@/contexts/toast-context"; import { useToast } from "@/contexts/toast-context";
import { useHostRuntimeSession } from "@/runtime/host-runtime"; import { useHostRuntimeClient } from "@/runtime/host-runtime";
import { import {
normalizeWorkspaceDescriptor, normalizeWorkspaceDescriptor,
useSessionStore, useSessionStore,
@@ -13,7 +13,7 @@ export function useOpenProject(
): (path: string) => Promise<boolean> { ): (path: string) => Promise<boolean> {
const normalizedServerId = serverId?.trim() ?? ""; const normalizedServerId = serverId?.trim() ?? "";
const toast = useToast(); const toast = useToast();
const { client } = useHostRuntimeSession(normalizedServerId); const client = useHostRuntimeClient(normalizedServerId);
const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces); const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces);
const setHasHydratedWorkspaces = useSessionStore( const setHasHydratedWorkspaces = useSessionStore(
(state) => state.setHasHydratedWorkspaces (state) => state.setHasHydratedWorkspaces
@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query"; 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"; import type { ProjectIcon } from "@server/shared/messages";
export function projectIconQueryKey(serverId: string, cwd: string) { export function projectIconQueryKey(serverId: string, cwd: string) {
@@ -12,7 +12,8 @@ interface UseProjectIconQueryOptions {
} }
export function useProjectIconQuery({ serverId, cwd }: UseProjectIconQueryOptions) { export function useProjectIconQuery({ serverId, cwd }: UseProjectIconQueryOptions) {
const { client, isConnected } = useHostRuntimeSession(serverId); const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const query = useQuery({ const query = useQuery({
queryKey: projectIconQueryKey(serverId, cwd), queryKey: projectIconQueryKey(serverId, cwd),
+13 -10
View File
@@ -23,7 +23,10 @@ import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
import { usePaneContext } from "@/panels/pane-context"; import { usePaneContext } from "@/panels/pane-context";
import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry"; import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry";
import { import {
useHostRuntimeSession, useHostRuntimeClient,
useHostRuntimeConnectionStatus,
useHostRuntimeIsConnected,
useHostRuntimeLastError,
useHosts, useHosts,
type HostRuntimeConnectionStatus, type HostRuntimeConnectionStatus,
} from "@/runtime/host-runtime"; } from "@/runtime/host-runtime";
@@ -154,11 +157,10 @@ function AgentPanelContent({
const resolvedServerId = serverId.trim() || undefined; const resolvedServerId = serverId.trim() || undefined;
const daemons = useHosts(); const daemons = useHosts();
const runtimeServerId = resolvedServerId ?? ""; const runtimeServerId = resolvedServerId ?? "";
const { const runtimeClient = useHostRuntimeClient(runtimeServerId);
snapshot: runtimeSnapshot, const runtimeIsConnected = useHostRuntimeIsConnected(runtimeServerId);
client: runtimeClient, const runtimeConnectionStatus = useHostRuntimeConnectionStatus(runtimeServerId);
isConnected: runtimeIsConnected, const runtimeLastError = useHostRuntimeLastError(runtimeServerId);
} = useHostRuntimeSession(runtimeServerId);
const connectionServerId = resolvedServerId ?? null; const connectionServerId = resolvedServerId ?? null;
const daemon = connectionServerId const daemon = connectionServerId
@@ -168,9 +170,10 @@ function AgentPanelContent({
daemon?.label ?? connectionServerId ?? "Selected host"; daemon?.label ?? connectionServerId ?? "Selected host";
const isUnknownDaemon = Boolean(connectionServerId && !daemon); const isUnknownDaemon = Boolean(connectionServerId && !daemon);
const connectionStatus: HostRuntimeConnectionStatus = const connectionStatus: HostRuntimeConnectionStatus =
runtimeSnapshot?.connectionStatus ?? isUnknownDaemon && runtimeConnectionStatus === "connecting"
(isUnknownDaemon ? "offline" : "connecting"); ? "offline"
const lastConnectionError = runtimeSnapshot?.lastError ?? null; : runtimeConnectionStatus;
const lastConnectionError = runtimeLastError;
if (!resolvedServerId || !runtimeClient) { if (!resolvedServerId || !runtimeClient) {
return ( return (
@@ -208,7 +211,7 @@ function AgentPanelBody({
serverId: string; serverId: string;
agentId?: string; agentId?: string;
isPaneFocused: boolean; isPaneFocused: boolean;
client: NonNullable<ReturnType<typeof useHostRuntimeSession>["client"]>; client: NonNullable<ReturnType<typeof useHostRuntimeClient>>;
isConnected: boolean; isConnected: boolean;
connectionStatus: HostRuntimeConnectionStatus; connectionStatus: HostRuntimeConnectionStatus;
onOpenWorkspaceFile?: (input: { filePath: string }) => void; onOpenWorkspaceFile?: (input: { filePath: string }) => void;
+47 -11
View File
@@ -1755,17 +1755,13 @@ export function useHostRuntimeSnapshot(
); );
} }
export function useHostRuntimeSession(serverId: string): { export function useHostRuntimeClient(serverId: string): DaemonClient | null {
snapshot: HostRuntimeSnapshot | null; const store = getHostRuntimeStore();
client: DaemonClient | null; return useSyncExternalStore(
isConnected: boolean; (onStoreChange) => store.subscribe(serverId, onStoreChange),
} { () => store.getSnapshot(serverId)?.client ?? null,
const snapshot = useHostRuntimeSnapshot(serverId); () => store.getSnapshot(serverId)?.client ?? null
return { );
snapshot,
client: snapshot?.client ?? null,
isConnected: isHostRuntimeConnected(snapshot),
};
} }
export function useHostRuntimeIsConnected(serverId: string): boolean { export function useHostRuntimeIsConnected(serverId: string): boolean {
@@ -1777,6 +1773,46 @@ export function useHostRuntimeIsConnected(serverId: string): boolean {
); );
} }
export function useHostRuntimeConnectionStatus(
serverId: string
): HostRuntimeConnectionStatus {
const store = getHostRuntimeStore();
return useSyncExternalStore(
(onStoreChange) => store.subscribe(serverId, onStoreChange),
() => store.getSnapshot(serverId)?.connectionStatus ?? "connecting",
() => store.getSnapshot(serverId)?.connectionStatus ?? "connecting"
);
}
export function useHostRuntimeLastError(serverId: string): string | null {
const store = getHostRuntimeStore();
return useSyncExternalStore(
(onStoreChange) => store.subscribe(serverId, onStoreChange),
() => store.getSnapshot(serverId)?.lastError ?? null,
() => store.getSnapshot(serverId)?.lastError ?? null
);
}
export function useHostRuntimeAgentDirectoryStatus(
serverId: string
): HostRuntimeAgentDirectoryStatus {
const store = getHostRuntimeStore();
return useSyncExternalStore(
(onStoreChange) => store.subscribe(serverId, onStoreChange),
() => store.getSnapshot(serverId)?.agentDirectoryStatus ?? "idle",
() => store.getSnapshot(serverId)?.agentDirectoryStatus ?? "idle"
);
}
export function useHostRuntimeIsDirectoryLoading(serverId: string): boolean {
const store = getHostRuntimeStore();
return useSyncExternalStore(
(onStoreChange) => store.subscribe(serverId, onStoreChange),
() => isHostRuntimeDirectoryLoading(store.getSnapshot(serverId)),
() => isHostRuntimeDirectoryLoading(store.getSnapshot(serverId))
);
}
export function useHosts(): HostProfile[] { export function useHosts(): HostProfile[] {
const store = getHostRuntimeStore(); const store = getHostRuntimeStore();
return useSyncExternalStore( return useSyncExternalStore(
@@ -33,7 +33,7 @@ import { buildWorkingDirectorySuggestions } from '@/utils/working-directory-sugg
import { useExplorerOpenGesture } from '@/hooks/use-explorer-open-gesture' import { useExplorerOpenGesture } from '@/hooks/use-explorer-open-gesture'
import { useSessionStore } from '@/stores/session-store' import { useSessionStore } from '@/stores/session-store'
import { generateDraftId } from '@/stores/draft-keys' import { generateDraftId } from '@/stores/draft-keys'
import { getHostRuntimeStore, useHostRuntimeSession } from '@/runtime/host-runtime' import { getHostRuntimeStore, useHostRuntimeClient, useHostRuntimeIsConnected } from '@/runtime/host-runtime'
import { ExplorerSidebarAnimationProvider } from '@/contexts/explorer-sidebar-animation-context' import { ExplorerSidebarAnimationProvider } from '@/contexts/explorer-sidebar-animation-context'
import { usePanelStore, type ExplorerCheckoutContext } from '@/stores/panel-store' import { usePanelStore, type ExplorerCheckoutContext } from '@/stores/panel-store'
import { MAX_CONTENT_WIDTH } from '@/constants/layout' import { MAX_CONTENT_WIDTH } from '@/constants/layout'
@@ -317,9 +317,8 @@ function DraftAgentScreenContent({
return collectAgentWorkingDirectorySuggestions([...liveSources, ...fetchedSources]) return collectAgentWorkingDirectorySuggestions([...liveSources, ...fetchedSources])
}, [allAgents, sessionAgents]) }, [allAgents, sessionAgents])
const { client: runtimeClient, isConnected: isHostOnline } = useHostRuntimeSession( const runtimeClient = useHostRuntimeClient(selectedServerId ?? '')
selectedServerId ?? '' const isHostOnline = useHostRuntimeIsConnected(selectedServerId ?? '')
)
const sessionClient = runtimeClient const sessionClient = runtimeClient
const trimmedWorkingDir = workingDir.trim() const trimmedWorkingDir = workingDir.trim()
const shouldInspectRepo = trimmedWorkingDir.length > 0 const shouldInspectRepo = trimmedWorkingDir.length > 0
+7 -5
View File
@@ -24,7 +24,9 @@ import { useSessionStore } from "@/stores/session-store";
import { import {
getHostRuntimeStore, getHostRuntimeStore,
isHostRuntimeConnected, isHostRuntimeConnected,
useHostRuntimeSession, useHostRuntimeClient,
useHostRuntimeIsConnected,
useHostRuntimeSnapshot,
} from "@/runtime/host-runtime"; } from "@/runtime/host-runtime";
import { AddHostMethodModal } from "@/components/add-host-method-modal"; import { AddHostMethodModal } from "@/components/add-host-method-modal";
import { AddHostModal } from "@/components/add-host-modal"; import { AddHostModal } from "@/components/add-host-modal";
@@ -992,9 +994,9 @@ function HostDetailModal({
const connections = host?.connections ?? []; const connections = host?.connections ?? [];
// Restart logic (moved from DaemonCard) // Restart logic (moved from DaemonCard)
const { snapshot: runtimeSnapshot, client: runtimeClient, isConnected } = useHostRuntimeSession( const runtimeSnapshot = useHostRuntimeSnapshot(host?.serverId ?? "");
host?.serverId ?? "" const runtimeClient = useHostRuntimeClient(host?.serverId ?? "");
); const isConnected = useHostRuntimeIsConnected(host?.serverId ?? "");
const runtime = getHostRuntimeStore(); const runtime = getHostRuntimeStore();
const daemonClient = runtimeClient; const daemonClient = runtimeClient;
const daemonVersion = useSessionStore((state) => host ? (state.sessions[host.serverId]?.serverInfo?.version ?? null) : null); const daemonVersion = useSessionStore((state) => host ? (state.sessions[host.serverId]?.serverInfo?.version ?? null) : null);
@@ -1389,7 +1391,7 @@ function DaemonCard({
onOpenSettings, onOpenSettings,
}: DaemonCardProps) { }: DaemonCardProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const { snapshot } = useHostRuntimeSession(daemon.serverId); const snapshot = useHostRuntimeSnapshot(daemon.serverId);
const connectionStatus = snapshot?.connectionStatus ?? "connecting"; const connectionStatus = snapshot?.connectionStatus ?? "connecting";
const activeConnection = snapshot?.activeConnection ?? null; const activeConnection = snapshot?.activeConnection ?? null;
const lastError = snapshot?.lastError ?? null; const lastError = snapshot?.lastError ?? null;
@@ -7,7 +7,7 @@ import { AgentStreamView } from '@/components/agent-stream-view'
import type { ImageAttachment } from '@/components/message-input' import type { ImageAttachment } from '@/components/message-input'
import { useAgentFormState } from '@/hooks/use-agent-form-state' import { useAgentFormState } from '@/hooks/use-agent-form-state'
import { useDraftAgentCreateFlow } from '@/hooks/use-draft-agent-create-flow' import { useDraftAgentCreateFlow } from '@/hooks/use-draft-agent-create-flow'
import { useHostRuntimeSession } from '@/runtime/host-runtime' import { useHostRuntimeClient, useHostRuntimeIsConnected } from '@/runtime/host-runtime'
import type { Agent } from '@/stores/session-store' import type { Agent } from '@/stores/session-store'
import { encodeImages } from '@/utils/encode-images' import { encodeImages } from '@/utils/encode-images'
import type { AgentCapabilityFlags, AgentSessionConfig } from '@server/server/agent/agent-sdk-types' import type { AgentCapabilityFlags, AgentSessionConfig } from '@server/server/agent/agent-sdk-types'
@@ -40,7 +40,8 @@ export function WorkspaceDraftAgentTab({
onCreated, onCreated,
onOpenWorkspaceFile, onOpenWorkspaceFile,
}: WorkspaceDraftAgentTabProps) { }: WorkspaceDraftAgentTabProps) {
const { client, isConnected } = useHostRuntimeSession(serverId) const client = useHostRuntimeClient(serverId)
const isConnected = useHostRuntimeIsConnected(serverId)
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null) const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null)
const { const {
@@ -70,7 +70,7 @@ import {
normalizeWorkspaceTabTarget, normalizeWorkspaceTabTarget,
workspaceTabTargetsEqual, workspaceTabTargetsEqual,
} from "@/utils/workspace-tab-identity"; } from "@/utils/workspace-tab-identity";
import { useHostRuntimeSession } from "@/runtime/host-runtime"; import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useWorkspaceTerminalSessionRetention } from "@/terminal/hooks/use-workspace-terminal-session-retention"; import { useWorkspaceTerminalSessionRetention } from "@/terminal/hooks/use-workspace-terminal-session-retention";
import { import {
checkoutStatusQueryKey, checkoutStatusQueryKey,
@@ -579,7 +579,8 @@ function WorkspaceScreenContent({
}); });
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { client, isConnected } = useHostRuntimeSession(normalizedServerId); const client = useHostRuntimeClient(normalizedServerId);
const isConnected = useHostRuntimeIsConnected(normalizedServerId);
const sessionAgents = useSessionStore( const sessionAgents = useSessionStore(
(state) => state.sessions[normalizedServerId]?.agents (state) => state.sessions[normalizedServerId]?.agents