From 72ea9b7a72abfdcd8091862653947858e1db6e68 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 20 Mar 2026 19:16:06 +0700 Subject: [PATCH] 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. --- packages/app/src/app/_layout.tsx | 4 +- .../src/app/h/[serverId]/agent/[agentId].tsx | 5 +- .../app/src/components/agent-input-area.tsx | 12 ++-- .../src/components/project-picker-modal.tsx | 5 +- packages/app/src/components/terminal-pane.tsx | 5 +- .../app/src/hooks/use-agent-autocomplete.ts | 5 +- .../app/src/hooks/use-agent-commands-query.ts | 5 +- .../app/src/hooks/use-agent-form-state.ts | 5 +- packages/app/src/hooks/use-all-agents-list.ts | 12 ++-- .../app/src/hooks/use-checkout-diff-query.ts | 5 +- .../src/hooks/use-checkout-pr-status-query.ts | 5 +- .../src/hooks/use-checkout-status-query.ts | 7 ++- packages/app/src/hooks/use-open-project.ts | 4 +- .../app/src/hooks/use-project-icon-query.ts | 5 +- packages/app/src/panels/agent-panel.tsx | 23 ++++---- packages/app/src/runtime/host-runtime.ts | 58 +++++++++++++++---- .../src/screens/agent/draft-agent-screen.tsx | 7 +-- packages/app/src/screens/settings-screen.tsx | 12 ++-- .../workspace/workspace-draft-agent-tab.tsx | 5 +- .../screens/workspace/workspace-screen.tsx | 5 +- 20 files changed, 124 insertions(+), 70 deletions(-) diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index ddd192c8d..b17164fcb 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -23,7 +23,7 @@ import { getHostRuntimeStore, useHosts, useHostMutations, - useHostRuntimeSession, + useHostRuntimeClient, } from "@/runtime/host-runtime"; import { SessionProvider } from "@/contexts/session-context"; import type { HostProfile } from "@/types/host-connection"; @@ -176,7 +176,7 @@ function PushNotificationRouter() { } function ManagedDaemonSession({ daemon }: { daemon: HostProfile }) { - const { client } = useHostRuntimeSession(daemon.serverId); + const client = useHostRuntimeClient(daemon.serverId); if (!client) { return null; diff --git a/packages/app/src/app/h/[serverId]/agent/[agentId].tsx b/packages/app/src/app/h/[serverId]/agent/[agentId].tsx index 4b7fb7401..54ef75896 100644 --- a/packages/app/src/app/h/[serverId]/agent/[agentId].tsx +++ b/packages/app/src/app/h/[serverId]/agent/[agentId].tsx @@ -1,7 +1,7 @@ import { useEffect, useRef } from "react"; import { useLocalSearchParams, useRouter } from "expo-router"; import { useSessionStore } from "@/stores/session-store"; -import { useHostRuntimeSession } from "@/runtime/host-runtime"; +import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; import { buildHostRootRoute, buildHostWorkspaceAgentRoute, @@ -16,7 +16,8 @@ export default function HostAgentReadyRoute() { const redirectedRef = useRef(false); const serverId = typeof params.serverId === "string" ? params.serverId : ""; 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) => { if (!serverId || !agentId) { return null; diff --git a/packages/app/src/components/agent-input-area.tsx b/packages/app/src/components/agent-input-area.tsx index 7ccc059c9..b0c922ec4 100644 --- a/packages/app/src/components/agent-input-area.tsx +++ b/packages/app/src/components/agent-input-area.tsx @@ -28,7 +28,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip import { Shortcut } from '@/components/ui/shortcut' import { Autocomplete } from '@/components/ui/autocomplete' import { useAgentAutocomplete } from '@/hooks/use-agent-autocomplete' -import { useHostRuntimeSession } from '@/runtime/host-runtime' +import { useHostRuntimeAgentDirectoryStatus, useHostRuntimeClient, useHostRuntimeIsConnected } from '@/runtime/host-runtime' import { deleteAttachments, persistAttachmentFromBlob, @@ -100,14 +100,16 @@ export function AgentInputArea({ const insets = useSafeAreaInsets() 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 voice = useVoiceOptional() const isDictationReady = isConnected && - (snapshot?.agentDirectoryStatus === 'ready' || - snapshot?.agentDirectoryStatus === 'revalidating' || - snapshot?.agentDirectoryStatus === 'error_after_ready') + (agentDirectoryStatus === 'ready' || + agentDirectoryStatus === 'revalidating' || + agentDirectoryStatus === 'error_after_ready') const agent = useSessionStore((state) => state.sessions[serverId]?.agents?.get(agentId)) diff --git a/packages/app/src/components/project-picker-modal.tsx b/packages/app/src/components/project-picker-modal.tsx index a1547ae43..c9ee903b3 100644 --- a/packages/app/src/components/project-picker-modal.tsx +++ b/packages/app/src/components/project-picker-modal.tsx @@ -15,7 +15,7 @@ import { usePathname } from "expo-router"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; import { shortenPath } from "@/utils/shorten-path"; 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 { parseServerIdFromPathname } from "@/utils/host-routes"; import { buildWorkingDirectorySuggestions } from "@/utils/working-directory-suggestions"; @@ -34,7 +34,8 @@ export function ProjectPickerModal() { return daemons[0]?.serverId ?? null; }, [pathname, daemons]); - const { client, isConnected } = useHostRuntimeSession(serverId ?? ""); + const client = useHostRuntimeClient(serverId ?? ""); + const isConnected = useHostRuntimeIsConnected(serverId ?? ""); const workspaces = useSessionStore((state) => serverId ? state.sessions[serverId]?.workspaces : undefined ); diff --git a/packages/app/src/components/terminal-pane.tsx b/packages/app/src/components/terminal-pane.tsx index f106b17e5..ada1657a8 100644 --- a/packages/app/src/components/terminal-pane.tsx +++ b/packages/app/src/components/terminal-pane.tsx @@ -19,7 +19,7 @@ import Svg, { import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import type { ListTerminalsResponse } from "@server/shared/messages"; 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 { hasPendingTerminalModifiers, @@ -150,7 +150,8 @@ export function TerminalPane({ }); 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 terminalsQueryKey = useMemo(() => ["terminals", serverId, cwd] as const, [cwd, serverId]); diff --git a/packages/app/src/hooks/use-agent-autocomplete.ts b/packages/app/src/hooks/use-agent-autocomplete.ts index eac27074c..491e0fe1a 100644 --- a/packages/app/src/hooks/use-agent-autocomplete.ts +++ b/packages/app/src/hooks/use-agent-autocomplete.ts @@ -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' diff --git a/packages/app/src/hooks/use-agent-commands-query.ts b/packages/app/src/hooks/use-agent-commands-query.ts index e77ab7d34..63c16e5d0 100644 --- a/packages/app/src/hooks/use-agent-commands-query.ts +++ b/packages/app/src/hooks/use-agent-commands-query.ts @@ -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), diff --git a/packages/app/src/hooks/use-agent-form-state.ts b/packages/app/src/hooks/use-agent-form-state.ts index 82c68a532..829ecfa8b 100644 --- a/packages/app/src/hooks/use-agent-form-state.ts +++ b/packages/app/src/hooks/use-agent-form-state.ts @@ -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], diff --git a/packages/app/src/hooks/use-all-agents-list.ts b/packages/app/src/hooks/use-all-agents-list.ts index 5c4ab7dce..77cd21fee 100644 --- a/packages/app/src/hooks/use-all-agents-list.ts +++ b/packages/app/src/hooks/use-all-agents-list.ts @@ -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; diff --git a/packages/app/src/hooks/use-checkout-diff-query.ts b/packages/app/src/hooks/use-checkout-diff-query.ts index d5d0c57ee..4e088e1db 100644 --- a/packages/app/src/hooks/use-checkout-diff-query.ts +++ b/packages/app/src/hooks/use-checkout-diff-query.ts @@ -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); diff --git a/packages/app/src/hooks/use-checkout-pr-status-query.ts b/packages/app/src/hooks/use-checkout-pr-status-query.ts index e4283505d..dcdfa2957 100644 --- a/packages/app/src/hooks/use-checkout-pr-status-query.ts +++ b/packages/app/src/hooks/use-checkout-pr-status-query.ts @@ -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), diff --git a/packages/app/src/hooks/use-checkout-status-query.ts b/packages/app/src/hooks/use-checkout-status-query.ts index dea647ea3..f1046c89e 100644 --- a/packages/app/src/hooks/use-checkout-status-query.ts +++ b/packages/app/src/hooks/use-checkout-status-query.ts @@ -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), diff --git a/packages/app/src/hooks/use-open-project.ts b/packages/app/src/hooks/use-open-project.ts index 8cad27f3b..2582e0552 100644 --- a/packages/app/src/hooks/use-open-project.ts +++ b/packages/app/src/hooks/use-open-project.ts @@ -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 { 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 diff --git a/packages/app/src/hooks/use-project-icon-query.ts b/packages/app/src/hooks/use-project-icon-query.ts index f34d98e42..6a5b5bbb9 100644 --- a/packages/app/src/hooks/use-project-icon-query.ts +++ b/packages/app/src/hooks/use-project-icon-query.ts @@ -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), diff --git a/packages/app/src/panels/agent-panel.tsx b/packages/app/src/panels/agent-panel.tsx index 2c4f545be..a2c17e684 100644 --- a/packages/app/src/panels/agent-panel.tsx +++ b/packages/app/src/panels/agent-panel.tsx @@ -23,7 +23,10 @@ import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; import { usePaneContext } from "@/panels/pane-context"; import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry"; import { - useHostRuntimeSession, + useHostRuntimeClient, + useHostRuntimeConnectionStatus, + useHostRuntimeIsConnected, + useHostRuntimeLastError, useHosts, type HostRuntimeConnectionStatus, } from "@/runtime/host-runtime"; @@ -154,11 +157,10 @@ function AgentPanelContent({ const resolvedServerId = serverId.trim() || undefined; const daemons = useHosts(); const runtimeServerId = resolvedServerId ?? ""; - const { - snapshot: runtimeSnapshot, - client: runtimeClient, - isConnected: runtimeIsConnected, - } = useHostRuntimeSession(runtimeServerId); + const runtimeClient = useHostRuntimeClient(runtimeServerId); + const runtimeIsConnected = useHostRuntimeIsConnected(runtimeServerId); + const runtimeConnectionStatus = useHostRuntimeConnectionStatus(runtimeServerId); + const runtimeLastError = useHostRuntimeLastError(runtimeServerId); const connectionServerId = resolvedServerId ?? null; const daemon = connectionServerId @@ -168,9 +170,10 @@ function AgentPanelContent({ daemon?.label ?? connectionServerId ?? "Selected host"; const isUnknownDaemon = Boolean(connectionServerId && !daemon); const connectionStatus: HostRuntimeConnectionStatus = - runtimeSnapshot?.connectionStatus ?? - (isUnknownDaemon ? "offline" : "connecting"); - const lastConnectionError = runtimeSnapshot?.lastError ?? null; + isUnknownDaemon && runtimeConnectionStatus === "connecting" + ? "offline" + : runtimeConnectionStatus; + const lastConnectionError = runtimeLastError; if (!resolvedServerId || !runtimeClient) { return ( @@ -208,7 +211,7 @@ function AgentPanelBody({ serverId: string; agentId?: string; isPaneFocused: boolean; - client: NonNullable["client"]>; + client: NonNullable>; isConnected: boolean; connectionStatus: HostRuntimeConnectionStatus; onOpenWorkspaceFile?: (input: { filePath: string }) => void; diff --git a/packages/app/src/runtime/host-runtime.ts b/packages/app/src/runtime/host-runtime.ts index fb31126f7..7db45073d 100644 --- a/packages/app/src/runtime/host-runtime.ts +++ b/packages/app/src/runtime/host-runtime.ts @@ -1755,17 +1755,13 @@ export function useHostRuntimeSnapshot( ); } -export function useHostRuntimeSession(serverId: string): { - snapshot: HostRuntimeSnapshot | null; - client: DaemonClient | null; - isConnected: boolean; -} { - const snapshot = useHostRuntimeSnapshot(serverId); - return { - snapshot, - client: snapshot?.client ?? null, - isConnected: isHostRuntimeConnected(snapshot), - }; +export function useHostRuntimeClient(serverId: string): DaemonClient | null { + const store = getHostRuntimeStore(); + return useSyncExternalStore( + (onStoreChange) => store.subscribe(serverId, onStoreChange), + () => store.getSnapshot(serverId)?.client ?? null, + () => store.getSnapshot(serverId)?.client ?? null + ); } 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[] { const store = getHostRuntimeStore(); return useSyncExternalStore( diff --git a/packages/app/src/screens/agent/draft-agent-screen.tsx b/packages/app/src/screens/agent/draft-agent-screen.tsx index 01e042361..cc902d188 100644 --- a/packages/app/src/screens/agent/draft-agent-screen.tsx +++ b/packages/app/src/screens/agent/draft-agent-screen.tsx @@ -33,7 +33,7 @@ import { buildWorkingDirectorySuggestions } from '@/utils/working-directory-sugg import { useExplorerOpenGesture } from '@/hooks/use-explorer-open-gesture' import { useSessionStore } from '@/stores/session-store' 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 { usePanelStore, type ExplorerCheckoutContext } from '@/stores/panel-store' import { MAX_CONTENT_WIDTH } from '@/constants/layout' @@ -317,9 +317,8 @@ function DraftAgentScreenContent({ return collectAgentWorkingDirectorySuggestions([...liveSources, ...fetchedSources]) }, [allAgents, sessionAgents]) - const { client: runtimeClient, isConnected: isHostOnline } = useHostRuntimeSession( - selectedServerId ?? '' - ) + const runtimeClient = useHostRuntimeClient(selectedServerId ?? '') + const isHostOnline = useHostRuntimeIsConnected(selectedServerId ?? '') const sessionClient = runtimeClient const trimmedWorkingDir = workingDir.trim() const shouldInspectRepo = trimmedWorkingDir.length > 0 diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index 9d04fdd9b..2d9d1a9ef 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -24,7 +24,9 @@ import { useSessionStore } from "@/stores/session-store"; import { getHostRuntimeStore, isHostRuntimeConnected, - useHostRuntimeSession, + useHostRuntimeClient, + useHostRuntimeIsConnected, + useHostRuntimeSnapshot, } from "@/runtime/host-runtime"; import { AddHostMethodModal } from "@/components/add-host-method-modal"; import { AddHostModal } from "@/components/add-host-modal"; @@ -992,9 +994,9 @@ function HostDetailModal({ const connections = host?.connections ?? []; // Restart logic (moved from DaemonCard) - const { snapshot: runtimeSnapshot, client: runtimeClient, isConnected } = useHostRuntimeSession( - host?.serverId ?? "" - ); + const runtimeSnapshot = useHostRuntimeSnapshot(host?.serverId ?? ""); + const runtimeClient = useHostRuntimeClient(host?.serverId ?? ""); + const isConnected = useHostRuntimeIsConnected(host?.serverId ?? ""); const runtime = getHostRuntimeStore(); const daemonClient = runtimeClient; const daemonVersion = useSessionStore((state) => host ? (state.sessions[host.serverId]?.serverInfo?.version ?? null) : null); @@ -1389,7 +1391,7 @@ function DaemonCard({ onOpenSettings, }: DaemonCardProps) { const { theme } = useUnistyles(); - const { snapshot } = useHostRuntimeSession(daemon.serverId); + const snapshot = useHostRuntimeSnapshot(daemon.serverId); const connectionStatus = snapshot?.connectionStatus ?? "connecting"; const activeConnection = snapshot?.activeConnection ?? null; const lastError = snapshot?.lastError ?? null; diff --git a/packages/app/src/screens/workspace/workspace-draft-agent-tab.tsx b/packages/app/src/screens/workspace/workspace-draft-agent-tab.tsx index 721417b99..3669d68e3 100644 --- a/packages/app/src/screens/workspace/workspace-draft-agent-tab.tsx +++ b/packages/app/src/screens/workspace/workspace-draft-agent-tab.tsx @@ -7,7 +7,7 @@ import { AgentStreamView } from '@/components/agent-stream-view' import type { ImageAttachment } from '@/components/message-input' import { useAgentFormState } from '@/hooks/use-agent-form-state' 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 { encodeImages } from '@/utils/encode-images' import type { AgentCapabilityFlags, AgentSessionConfig } from '@server/server/agent/agent-sdk-types' @@ -40,7 +40,8 @@ export function WorkspaceDraftAgentTab({ onCreated, onOpenWorkspaceFile, }: WorkspaceDraftAgentTabProps) { - const { client, isConnected } = useHostRuntimeSession(serverId) + const client = useHostRuntimeClient(serverId) + const isConnected = useHostRuntimeIsConnected(serverId) const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null) const { diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index 103abb5ec..a61eb9a5c 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -70,7 +70,7 @@ import { normalizeWorkspaceTabTarget, workspaceTabTargetsEqual, } 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 { checkoutStatusQueryKey, @@ -579,7 +579,8 @@ function WorkspaceScreenContent({ }); const queryClient = useQueryClient(); - const { client, isConnected } = useHostRuntimeSession(normalizedServerId); + const client = useHostRuntimeClient(normalizedServerId); + const isConnected = useHostRuntimeIsConnected(normalizedServerId); const sessionAgents = useSessionStore( (state) => state.sessions[normalizedServerId]?.agents