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 ddd192c8da..b17164fcbc 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 4b7fb74011..54ef758961 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 7ccc059c99..b0c922ec4f 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 a1547ae435..c9ee903b39 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 f106b17e54..ada1657a83 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 eac27074ca..491e0fe1ac 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 e77ab7d345..63c16e5d08 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 82c68a5322..829ecfa8b4 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 5c4ab7dcea..77cd21feed 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 d5d0c57ee4..4e088e1db4 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 e4283505d8..dcdfa2957d 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 dea647ea37..f1046c89e8 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 8cad27f3b5..2582e05528 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 f34d98e42a..6a5b5bbb99 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 2c4f545bef..a2c17e6844 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 fb31126f7f..7db45073df 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 01e0423617..cc902d188d 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 9d04fdd9b1..2d9d1a9efb 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 721417b991..3669d68e3d 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 103abb5ec0..a61eb9a5ce 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