Unify app connection state around host runtime

This commit is contained in:
Mohamed Boudra
2026-02-20 10:37:59 +07:00
parent 6b23509b96
commit d128a32d2f
22 changed files with 173 additions and 191 deletions
+7 -3
View File
@@ -18,6 +18,10 @@ import { shortenPath } from "@/utils/shorten-path";
import { deriveBranchLabel, deriveProjectPath } from "@/utils/agent-display-info"; import { deriveBranchLabel, deriveProjectPath } from "@/utils/agent-display-info";
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents"; import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import {
getHostRuntimeStore,
isHostRuntimeConnected,
} from "@/runtime/host-runtime";
import { AgentStatusDot } from "@/components/agent-status-dot"; import { AgentStatusDot } from "@/components/agent-status-dot";
import { import {
CHECKOUT_STATUS_STALE_TIME, CHECKOUT_STATUS_STALE_TIME,
@@ -154,9 +158,9 @@ export function AgentList({
continue; continue;
} }
const session = useSessionStore.getState().sessions[agent.serverId]; const runtime = getHostRuntimeStore();
const client = session?.client ?? null; const client = runtime.getClient(agent.serverId);
const isConnected = session?.connection.isConnected ?? false; const isConnected = isHostRuntimeConnected(runtime.getSnapshot(agent.serverId));
if (!client || !isConnected) { if (!client || !isConnected) {
continue; continue;
} }
@@ -24,7 +24,7 @@ function ManagedDaemonSession({ daemon }: { daemon: HostProfile }) {
); );
}, [daemon.serverId, hasConnectionRecord, snapshot, updateConnectionStatus]); }, [daemon.serverId, hasConnectionRecord, snapshot, updateConnectionStatus]);
if (!snapshot?.activeConnection || !client) { if (!client) {
return null; return null;
} }
@@ -28,7 +28,10 @@ import {
DraggableList, DraggableList,
type DraggableRenderItemInfo, type DraggableRenderItemInfo,
} from "./draggable-list"; } from "./draggable-list";
import { useSessionStore } from "@/stores/session-store"; import {
getHostRuntimeStore,
isHostRuntimeConnected,
} from "@/runtime/host-runtime";
import { import {
buildAgentNavigationKey, buildAgentNavigationKey,
startNavigationTiming, startNavigationTiming,
@@ -490,7 +493,6 @@ export function SidebarAgentList({
}, [projectOptions, selectedProjectKeys]); }, [projectOptions, selectedProjectKeys]);
const showProjectFilters = projectOptions.length > 0; const showProjectFilters = projectOptions.length > 0;
const sessions = useSessionStore((state) => state.sessions);
const projectIconRequests = useMemo(() => { const projectIconRequests = useMemo(() => {
const unique = new Map<string, { serverId: string; cwd: string }>(); const unique = new Map<string, { serverId: string; cwd: string }>();
for (const option of projectOptions) { for (const option of projectOptions) {
@@ -517,8 +519,7 @@ export function SidebarAgentList({
queries: projectIconRequests.map((request) => ({ queries: projectIconRequests.map((request) => ({
queryKey: projectIconQueryKey(request.serverId, request.cwd), queryKey: projectIconQueryKey(request.serverId, request.cwd),
queryFn: async () => { queryFn: async () => {
const client = const client = getHostRuntimeStore().getClient(request.serverId);
useSessionStore.getState().sessions[request.serverId]?.client ?? null;
if (!client) { if (!client) {
return null; return null;
} }
@@ -526,8 +527,10 @@ export function SidebarAgentList({
return result.icon; return result.icon;
}, },
enabled: Boolean( enabled: Boolean(
sessions[request.serverId]?.client && getHostRuntimeStore().getClient(request.serverId) &&
sessions[request.serverId]?.connection.isConnected && isHostRuntimeConnected(
getHostRuntimeStore().getSnapshot(request.serverId)
) &&
request.cwd request.cwd
), ),
staleTime: Infinity, staleTime: Infinity,
@@ -17,7 +17,7 @@ import Svg, {
} from "react-native-svg"; } from "react-native-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 { useSessionStore } from "@/stores/session-store"; import { useHostRuntimeSession } from "@/runtime/host-runtime";
import { import {
hasPendingTerminalModifiers, hasPendingTerminalModifiers,
normalizeTerminalTransportKey, normalizeTerminalTransportKey,
@@ -135,10 +135,7 @@ export function TerminalPane({ serverId, cwd }: TerminalPaneProps) {
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null); const { client, isConnected } = useHostRuntimeSession(serverId);
const isConnected = useSessionStore(
(state) => state.sessions[serverId]?.connection.isConnected ?? false
);
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]);
@@ -18,7 +18,7 @@ export type ConnectionStatus = ConnectionState["status"];
export type ConnectionStateUpdate = export type ConnectionStateUpdate =
| { status: "idle" } | { status: "idle" }
| { status: "connecting"; activeConnection?: ActiveConnection | null; lastOnlineAt?: string | null } | { status: "connecting"; activeConnection?: ActiveConnection | null; lastOnlineAt?: string | null }
| { status: "online"; activeConnection?: ActiveConnection | null; lastOnlineAt: string; agentListReady?: boolean } | { status: "online"; activeConnection?: ActiveConnection | null; lastOnlineAt: string }
| { status: "offline"; activeConnection?: ActiveConnection | null; lastError?: string | null; lastOnlineAt?: string | null } | { status: "offline"; activeConnection?: ActiveConnection | null; lastError?: string | null; lastOnlineAt?: string | null }
| { status: "error"; activeConnection?: ActiveConnection | null; lastError: string; lastOnlineAt?: string | null }; | { status: "error"; activeConnection?: ActiveConnection | null; lastError: string; lastOnlineAt?: string | null };
@@ -30,6 +30,7 @@ interface DaemonConnectionsContextValue {
connectionStates: Map<string, DaemonConnectionRecord>; connectionStates: Map<string, DaemonConnectionRecord>;
isLoading: boolean; isLoading: boolean;
updateConnectionStatus: (serverId: string, update: ConnectionStateUpdate) => void; updateConnectionStatus: (serverId: string, update: ConnectionStateUpdate) => void;
markAgentListReady: (serverId: string, ready: boolean) => void;
} }
const DaemonConnectionsContext = createContext<DaemonConnectionsContextValue | null>(null); const DaemonConnectionsContext = createContext<DaemonConnectionsContextValue | null>(null);
@@ -69,7 +70,8 @@ function resolveNextConnectionState(
hasEverReceivedAgentList: existing.hasEverReceivedAgentList ?? false, hasEverReceivedAgentList: existing.hasEverReceivedAgentList ?? false,
}; };
case "online": case "online":
const currentAgentListReady = update.agentListReady ?? (existing.status === "online" ? existing.agentListReady : false); const currentAgentListReady =
existing.status === "online" ? existing.agentListReady : false;
return { return {
status: "online", status: "online",
@@ -173,10 +175,37 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode })
[] []
); );
const markAgentListReady = useCallback((serverId: string, ready: boolean) => {
setConnectionStates((prev) => {
const existing = prev.get(serverId);
if (!existing) {
return prev;
}
if (existing.status !== "online") {
return prev;
}
if (
existing.agentListReady === ready &&
(ready ? existing.hasEverReceivedAgentList : true)
) {
return prev;
}
const next = new Map(prev);
next.set(serverId, {
...existing,
agentListReady: ready,
hasEverReceivedAgentList:
ready || existing.hasEverReceivedAgentList,
});
return next;
});
}, []);
const value: DaemonConnectionsContextValue = { const value: DaemonConnectionsContextValue = {
connectionStates, connectionStates,
isLoading: registryLoading, isLoading: registryLoading,
updateConnectionStatus, updateConnectionStatus,
markAgentListReady,
}; };
return ( return (
+11 -18
View File
@@ -33,7 +33,6 @@ import {
useSessionStore, useSessionStore,
type Agent, type Agent,
type SessionState, type SessionState,
type DaemonConnectionSnapshot,
} from "@/stores/session-store"; } from "@/stores/session-store";
import { useDraftStore } from "@/stores/draft-store"; import { useDraftStore } from "@/stores/draft-store";
import type { AgentDirectoryEntry } from "@/types/agent-directory"; import type { AgentDirectoryEntry } from "@/types/agent-directory";
@@ -74,6 +73,12 @@ const derivePendingPermissionKey = (
const HISTORY_STALE_AFTER_MS = 60_000; const HISTORY_STALE_AFTER_MS = 60_000;
type SessionConnectionSnapshot = {
isConnected: boolean;
isConnecting: boolean;
lastError: string | null;
};
const findLatestAssistantMessageText = (items: StreamItem[]): string | null => { const findLatestAssistantMessageText = (items: StreamItem[]): string | null => {
for (let i = items.length - 1; i >= 0; i -= 1) { for (let i = items.length - 1; i >= 0; i -= 1) {
const item = items[i]; const item = items[i];
@@ -87,7 +92,7 @@ const findLatestAssistantMessageText = (items: StreamItem[]): string | null => {
const mapConnectionState = ( const mapConnectionState = (
state: ConnectionState, state: ConnectionState,
lastError: string | null lastError: string | null
): DaemonConnectionSnapshot => ({ ): SessionConnectionSnapshot => ({
isConnected: state.status === "connected", isConnected: state.status === "connected",
isConnecting: state.status === "connecting", isConnecting: state.status === "connecting",
lastError: state.status === "disconnected" ? state.reason ?? lastError : null, lastError: state.status === "disconnected" ? state.reason ?? lastError : null,
@@ -264,10 +269,10 @@ function SessionProviderInternal({
}: SessionProviderClientProps) { }: SessionProviderClientProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [connectionSnapshot, setConnectionSnapshot] = const [connectionSnapshot, setConnectionSnapshot] =
useState<DaemonConnectionSnapshot>(() => useState<SessionConnectionSnapshot>(() =>
mapConnectionState(client.getConnectionState(), client.lastError) mapConnectionState(client.getConnectionState(), client.lastError)
); );
const { updateConnectionStatus } = useDaemonConnections(); const { markAgentListReady } = useDaemonConnections();
// Zustand store actions // Zustand store actions
const initializeSession = useSessionStore((state) => state.initializeSession); const initializeSession = useSessionStore((state) => state.initializeSession);
@@ -312,9 +317,6 @@ function SessionProviderInternal({
const clearDraftInput = useDraftStore((state) => state.clearDraftInput); const clearDraftInput = useDraftStore((state) => state.clearDraftInput);
const setQueuedMessages = useSessionStore((state) => state.setQueuedMessages); const setQueuedMessages = useSessionStore((state) => state.setQueuedMessages);
const updateSessionClient = useSessionStore((state) => state.updateSessionClient); const updateSessionClient = useSessionStore((state) => state.updateSessionClient);
const updateSessionConnection = useSessionStore(
(state) => state.updateSessionConnection
);
const updateSessionServerInfo = useSessionStore((state) => state.updateSessionServerInfo); const updateSessionServerInfo = useSessionStore((state) => state.updateSessionServerInfo);
// Track focused agent for heartbeat // Track focused agent for heartbeat
@@ -470,10 +472,6 @@ function SessionProviderInternal({
wasConnectedRef.current = connectionSnapshot.isConnected; wasConnectedRef.current = connectionSnapshot.isConnected;
}, [serverId, connectionSnapshot.isConnected, bumpHistorySyncGeneration]); }, [serverId, connectionSnapshot.isConnected, bumpHistorySyncGeneration]);
useEffect(() => {
updateSessionConnection(serverId, connectionSnapshot);
}, [serverId, connectionSnapshot, updateSessionConnection]);
// Initialize session in store // Initialize session in store
useEffect(() => { useEffect(() => {
initializeSession(serverId, client, audioPlayer); initializeSession(serverId, client, audioPlayer);
@@ -855,11 +853,7 @@ function SessionProviderInternal({
setPendingPermissions(serverId, nextPendingPermissions); setPendingPermissions(serverId, nextPendingPermissions);
setInitializingAgents(serverId, new Map()); setInitializingAgents(serverId, new Map());
setHasHydratedAgents(serverId, true); setHasHydratedAgents(serverId, true);
updateConnectionStatus(serverId, { markAgentListReady(serverId, true);
status: "online",
lastOnlineAt: new Date().toISOString(),
agentListReady: true,
});
} catch (err) { } catch (err) {
if (cancelled) { if (cancelled) {
return; return;
@@ -885,7 +879,7 @@ function SessionProviderInternal({
setHasHydratedAgents, setHasHydratedAgents,
setInitializingAgents, setInitializingAgents,
setPendingPermissions, setPendingPermissions,
updateConnectionStatus, markAgentListReady,
]); ]);
// Daemon message handlers - directly update Zustand store // Daemon message handlers - directly update Zustand store
@@ -1420,7 +1414,6 @@ function SessionProviderInternal({
setPendingPermissions, setPendingPermissions,
setFileExplorer, setFileExplorer,
setHasHydratedAgents, setHasHydratedAgents,
updateConnectionStatus,
clearDraftInput, clearDraftInput,
notifyAgentAttention, notifyAgentAttention,
applyAgentUpdatePayload, applyAgentUpdatePayload,
+7 -3
View File
@@ -2,6 +2,7 @@ import { createContext, useContext, useState, ReactNode, useCallback, useEffect,
import { useSpeechmaticsAudio } from "@/hooks/use-speechmatics-audio"; import { useSpeechmaticsAudio } from "@/hooks/use-speechmatics-audio";
import type { SessionState } from "@/stores/session-store"; import type { SessionState } from "@/stores/session-store";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import { useHostRuntimeSession } from "@/runtime/host-runtime";
import { resolveVoiceUnavailableMessage } from "@/utils/server-info-capabilities"; import { resolveVoiceUnavailableMessage } from "@/utils/server-info-capabilities";
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake"; import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
import { REALTIME_VOICE_VAD_CONFIG } from "@/voice/realtime-voice-config"; import { REALTIME_VOICE_VAD_CONFIG } from "@/voice/realtime-voice-config";
@@ -45,6 +46,9 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
const getSession = useSessionStore((state) => state.getSession); const getSession = useSessionStore((state) => state.getSession);
const [activeServerId, setActiveServerId] = useState<string | null>(null); const [activeServerId, setActiveServerId] = useState<string | null>(null);
const [activeAgentId, setActiveAgentId] = useState<string | null>(null); const [activeAgentId, setActiveAgentId] = useState<string | null>(null);
const { client: runtimeClient, isConnected: activeRuntimeConnected } = useHostRuntimeSession(
activeServerId ?? ""
);
const activeSession = useSessionStore( const activeSession = useSessionStore(
useCallback( useCallback(
(state: ReturnType<typeof useSessionStore.getState>) => { (state: ReturnType<typeof useSessionStore.getState>) => {
@@ -274,8 +278,8 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
}, [isVoiceMode, realtimeAudio.isDetecting, realtimeAudio.isSpeaking]); }, [isVoiceMode, realtimeAudio.isDetecting, realtimeAudio.isSpeaking]);
useEffect(() => { useEffect(() => {
const connected = activeSession?.connection.isConnected ?? false; const connected = activeRuntimeConnected;
const client = activeSession?.client ?? null; const client = runtimeClient;
if (!connected) { if (!connected) {
voiceTransportReadyRef.current = false; voiceTransportReadyRef.current = false;
} }
@@ -313,7 +317,7 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
} }
wasVoiceSocketConnectedRef.current = connected; wasVoiceSocketConnectedRef.current = connected;
}, [activeAgentId, activeServerId, activeSession?.client, activeSession?.connection.isConnected, isVoiceMode]); }, [activeAgentId, activeRuntimeConnected, activeServerId, isVoiceMode, runtimeClient]);
const isPlayingAudio = activeSession?.isPlayingAudio ?? false; const isPlayingAudio = activeSession?.isPlayingAudio ?? false;
@@ -5,6 +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 { import {
applyFileMentionReplacement, applyFileMentionReplacement,
findActiveFileMention, findActiveFileMention,
@@ -137,10 +138,7 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut
return queryDraftConfig?.cwd ?? '' return queryDraftConfig?.cwd ?? ''
}, [agentCwd, isRealAgent, queryDraftConfig]) }, [agentCwd, isRealAgent, queryDraftConfig])
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null) const { client, isConnected } = useHostRuntimeSession(serverId)
const isConnected = useSessionStore(
(state) => state.sessions[serverId]?.connection.isConnected ?? false
)
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 { useSessionStore } from "@/stores/session-store"; import { useHostRuntimeSession } 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,12 +48,7 @@ export function useAgentCommandsQuery({
enabled = true, enabled = true,
draftConfig, draftConfig,
}: UseAgentCommandsQueryOptions) { }: UseAgentCommandsQueryOptions) {
const client = useSessionStore( const { client, isConnected } = useHostRuntimeSession(serverId);
(state) => state.sessions[serverId]?.client ?? null
);
const isConnected = useSessionStore(
(state) => state.sessions[serverId]?.connection.isConnected ?? false
);
const query = useQuery({ const query = useQuery({
queryKey: commandsQueryKey(serverId, agentId, draftConfig), queryKey: commandsQueryKey(serverId, agentId, draftConfig),
@@ -9,8 +9,8 @@ import type {
AgentModelDefinition, AgentModelDefinition,
AgentProvider, AgentProvider,
} from "@server/server/agent/agent-sdk-types"; } from "@server/server/agent/agent-sdk-types";
import { useSessionStore } from "@/stores/session-store";
import { useDaemonRegistry } from "@/contexts/daemon-registry-context"; import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
import { useHostRuntimeSession } 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
@@ -364,11 +364,7 @@ export function useAgentFormState(
}, [isVisible]); }, [isVisible]);
// Session state for provider model listing // Session state for provider model listing
const sessionState = useSessionStore((state) => const { client, isConnected } = useHostRuntimeSession(formState.serverId ?? "");
formState.serverId ? state.sessions[formState.serverId] : undefined
);
const client = sessionState?.client ?? null;
const isConnected = sessionState?.connection?.isConnected ?? false;
const availableProvidersQuery = useQuery({ const availableProvidersQuery = useQuery({
queryKey: ["availableProviders", formState.serverId], queryKey: ["availableProviders", formState.serverId],
@@ -1,6 +1,9 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import type { FetchAgentTimelineOptions } from "@server/client/daemon-client"; import type {
DaemonClient,
FetchAgentTimelineOptions,
} from "@server/client/daemon-client";
import { import {
attachInitTimeout, attachInitTimeout,
createInitDeferred, createInitDeferred,
@@ -37,8 +40,13 @@ function buildInitialTimelineRequest(
}; };
} }
export function useAgentInitialization(serverId: string) { export function useAgentInitialization({
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null); serverId,
client,
}: {
serverId: string;
client: DaemonClient | null;
}) {
const setInitializingAgents = useSessionStore((state) => state.setInitializingAgents); const setInitializingAgents = useSessionStore((state) => state.setInitializingAgents);
const setAgentInitializing = useCallback( const setAgentInitializing = useCallback(
(agentId: string, initializing: boolean) => { (agentId: string, initializing: boolean) => {
@@ -2,6 +2,7 @@ import { useCallback, useMemo } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useDaemonConnections } from "@/contexts/daemon-connections-context"; import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import { useSessionStore, type Agent } from "@/stores/session-store"; import { useSessionStore, type Agent } from "@/stores/session-store";
import { useHostRuntimeSession } from "@/runtime/host-runtime";
import type { AggregatedAgent, AggregatedAgentsResult } from "@/hooks/use-aggregated-agents"; import type { AggregatedAgent, AggregatedAgentsResult } from "@/hooks/use-aggregated-agents";
import { normalizeAgentSnapshot } from "@/utils/agent-snapshots"; import { normalizeAgentSnapshot } from "@/utils/agent-snapshots";
@@ -45,8 +46,7 @@ export function useAllAgentsList(options?: {
const session = useSessionStore((state) => const session = useSessionStore((state) =>
serverId ? state.sessions[serverId] : undefined serverId ? state.sessions[serverId] : undefined
); );
const client = session?.client ?? null; const { client, isConnected } = useHostRuntimeSession(serverId ?? "");
const isConnected = session?.connection.isConnected ?? false;
const liveAgents = session?.agents ?? null; const liveAgents = session?.agents ?? null;
const canFetch = Boolean(serverId && client && isConnected); const canFetch = Boolean(serverId && client && isConnected);
@@ -1,8 +1,8 @@
import { useQuery, useQueryClient } from "@tanstack/react-query"; 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 { useSessionStore } from "@/stores/session-store";
import { usePanelStore } from "@/stores/panel-store"; import { usePanelStore } from "@/stores/panel-store";
import { useHostRuntimeSession } 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,12 +56,7 @@ export function useCheckoutDiffQuery({
enabled = true, enabled = true,
}: UseCheckoutDiffQueryOptions) { }: UseCheckoutDiffQueryOptions) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const client = useSessionStore( const { client, isConnected } = useHostRuntimeSession(serverId);
(state) => state.sessions[serverId]?.client ?? null
);
const isConnected = useSessionStore(
(state) => state.sessions[serverId]?.connection.isConnected ?? false
);
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 { useSessionStore } from "@/stores/session-store"; import { useHostRuntimeSession } 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,12 +21,7 @@ export function useCheckoutPrStatusQuery({
cwd, cwd,
enabled = true, enabled = true,
}: UseCheckoutPrStatusQueryOptions) { }: UseCheckoutPrStatusQueryOptions) {
const client = useSessionStore( const { client, isConnected } = useHostRuntimeSession(serverId);
(state) => state.sessions[serverId]?.client ?? null
);
const isConnected = useSessionStore(
(state) => state.sessions[serverId]?.connection.isConnected ?? false
);
const query = useQuery({ const query = useQuery({
queryKey: checkoutPrStatusQueryKey(serverId, cwd), queryKey: checkoutPrStatusQueryKey(serverId, cwd),
@@ -1,8 +1,8 @@
import { useQuery } from "@tanstack/react-query"; 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 { useSessionStore } from "@/stores/session-store";
import { usePanelStore } from "@/stores/panel-store"; import { usePanelStore } from "@/stores/panel-store";
import { useHostRuntimeSession } from "@/runtime/host-runtime";
import type { CheckoutStatusResponse } from "@server/shared/messages"; import type { CheckoutStatusResponse } from "@server/shared/messages";
import { import {
checkoutStatusRevalidationKey, checkoutStatusRevalidationKey,
@@ -30,12 +30,7 @@ function fetchCheckoutStatus(
} }
export function useCheckoutStatusQuery({ serverId, cwd }: UseCheckoutStatusQueryOptions) { export function useCheckoutStatusQuery({ serverId, cwd }: UseCheckoutStatusQueryOptions) {
const client = useSessionStore( const { client, isConnected } = useHostRuntimeSession(serverId);
(state) => state.sessions[serverId]?.client ?? null
);
const isConnected = useSessionStore(
(state) => state.sessions[serverId]?.connection.isConnected ?? false
);
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);
@@ -92,9 +87,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 = useSessionStore( const { client } = useHostRuntimeSession(serverId);
(state) => state.sessions[serverId]?.client ?? null
);
return useQuery({ return useQuery({
queryKey: checkoutStatusQueryKey(serverId, cwd), queryKey: checkoutStatusQueryKey(serverId, cwd),
@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useSessionStore } from "@/stores/session-store"; import { useHostRuntimeSession } 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,12 +12,7 @@ interface UseProjectIconQueryOptions {
} }
export function useProjectIconQuery({ serverId, cwd }: UseProjectIconQueryOptions) { export function useProjectIconQuery({ serverId, cwd }: UseProjectIconQueryOptions) {
const client = useSessionStore( const { client, isConnected } = useHostRuntimeSession(serverId);
(state) => state.sessions[serverId]?.client ?? null
);
const isConnected = useSessionStore(
(state) => state.sessions[serverId]?.connection.isConnected ?? false
);
const query = useQuery({ const query = useQuery({
queryKey: projectIconQueryKey(serverId, cwd), queryKey: projectIconQueryKey(serverId, cwd),
@@ -2,6 +2,7 @@ import { useCallback, useMemo } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useDaemonConnections } from "@/contexts/daemon-connections-context"; import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import { useSessionStore, type Agent } from "@/stores/session-store"; import { useSessionStore, type Agent } from "@/stores/session-store";
import { useHostRuntimeSession } from "@/runtime/host-runtime";
import type { AggregatedAgent } from "@/hooks/use-aggregated-agents"; import type { AggregatedAgent } from "@/hooks/use-aggregated-agents";
import { normalizeAgentSnapshot } from "@/utils/agent-snapshots"; import { normalizeAgentSnapshot } from "@/utils/agent-snapshots";
import { import {
@@ -170,9 +171,8 @@ export function useSidebarAgentsGrouped(options?: {
const session = useSessionStore((state) => const session = useSessionStore((state) =>
serverId ? state.sessions[serverId] : undefined serverId ? state.sessions[serverId] : undefined
); );
const client = session?.client ?? null; const { client, isConnected } = useHostRuntimeSession(serverId ?? "");
const liveAgents = session?.agents ?? null; const liveAgents = session?.agents ?? null;
const isConnected = session?.connection.isConnected ?? false;
const canFetch = Boolean(serverId && client && isConnected); const canFetch = Boolean(serverId && client && isConnected);
const agentsQuery = useQuery({ const agentsQuery = useQuery({
+8
View File
@@ -32,6 +32,12 @@ export type HostRuntimeSnapshot = {
clientGeneration: number; clientGeneration: number;
}; };
export function isHostRuntimeConnected(
snapshot: HostRuntimeSnapshot | null
): boolean {
return snapshot?.connectionStatus === "online";
}
export type HostRuntimeControllerDeps = { export type HostRuntimeControllerDeps = {
createClient: (input: { createClient: (input: {
host: HostProfile; host: HostProfile;
@@ -564,6 +570,7 @@ export function useHostRuntimeSnapshot(
export function useHostRuntimeSession(serverId: string): { export function useHostRuntimeSession(serverId: string): {
snapshot: HostRuntimeSnapshot | null; snapshot: HostRuntimeSnapshot | null;
client: DaemonClient | null; client: DaemonClient | null;
isConnected: boolean;
} { } {
const store = getHostRuntimeStore(); const store = getHostRuntimeStore();
const snapshot = useHostRuntimeSnapshot(serverId); const snapshot = useHostRuntimeSnapshot(serverId);
@@ -571,6 +578,7 @@ export function useHostRuntimeSession(serverId: string): {
() => ({ () => ({
snapshot, snapshot,
client: store.getClient(serverId), client: store.getClient(serverId),
isConnected: isHostRuntimeConnected(snapshot),
}), }),
[serverId, snapshot, store] [serverId, snapshot, store]
); );
@@ -41,8 +41,11 @@ import {
} from "@/contexts/explorer-sidebar-animation-context"; } from "@/contexts/explorer-sidebar-animation-context";
import { usePanelStore } from "@/stores/panel-store"; import { usePanelStore } from "@/stores/panel-store";
import { useDaemonConnections } from "@/contexts/daemon-connections-context"; import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import {
useHostRuntimeSession,
type HostRuntimeConnectionStatus,
} from "@/runtime/host-runtime";
import { useCreateFlowStore } from "@/stores/create-flow-store"; import { useCreateFlowStore } from "@/stores/create-flow-store";
import type { Agent } from "@/contexts/session-context"; import type { Agent } from "@/contexts/session-context";
import type { StreamItem } from "@/types/stream"; import type { StreamItem } from "@/types/stream";
@@ -75,7 +78,7 @@ import {
} from "@/utils/agent-snapshots"; } from "@/utils/agent-snapshots";
import { mergePendingCreateImages } from "@/utils/pending-create-images"; import { mergePendingCreateImages } from "@/utils/pending-create-images";
import { shouldClearAgentAttentionOnView } from "@/utils/agent-attention"; import { shouldClearAgentAttentionOnView } from "@/utils/agent-attention";
import type { FetchAgentsEntry } from "@server/client/daemon-client"; import type { DaemonClient, FetchAgentsEntry } from "@server/client/daemon-client";
import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture"; import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture";
import { import {
DropdownMenu, DropdownMenu,
@@ -109,11 +112,12 @@ export function AgentReadyScreen({
const resolvedAgentId = agentId?.trim() || undefined; const resolvedAgentId = agentId?.trim() || undefined;
const resolvedServerId = serverId?.trim() || undefined; const resolvedServerId = serverId?.trim() || undefined;
const { connectionStates } = useDaemonConnections(); const { connectionStates } = useDaemonConnections();
const runtimeServerId = resolvedServerId ?? "";
// Check if session exists for this serverId const {
const hasSession = useSessionStore((state) => snapshot: runtimeSnapshot,
resolvedServerId ? !!state.sessions[resolvedServerId] : false client: runtimeClient,
); isConnected: runtimeIsConnected,
} = useHostRuntimeSession(runtimeServerId);
const connectionServerId = resolvedServerId ?? null; const connectionServerId = resolvedServerId ?? null;
const connection = connectionServerId const connection = connectionServerId
@@ -122,9 +126,11 @@ export function AgentReadyScreen({
const serverLabel = const serverLabel =
connection?.daemon.label ?? connectionServerId ?? "Selected host"; connection?.daemon.label ?? connectionServerId ?? "Selected host";
const isUnknownDaemon = Boolean(connectionServerId && !connection); const isUnknownDaemon = Boolean(connectionServerId && !connection);
const connectionStatus = const connectionStatus: HostRuntimeConnectionStatus =
connection?.status ?? (isUnknownDaemon ? "offline" : "idle"); runtimeSnapshot?.connectionStatus ??
const lastConnectionError = connection?.lastError ?? null; (isUnknownDaemon ? "offline" : "idle");
const lastConnectionError = runtimeSnapshot?.lastError ?? null;
const isRuntimeSessionAvailable = Boolean(resolvedServerId && runtimeClient);
const handleBackToHome = useCallback(() => { const handleBackToHome = useCallback(() => {
const targetServerId = resolvedServerId; const targetServerId = resolvedServerId;
@@ -154,7 +160,9 @@ export function AgentReadyScreen({
}, [resolvedAgentId, resolvedServerId, router]); }, [resolvedAgentId, resolvedServerId, router]);
const focusServerId = resolvedServerId; const focusServerId = resolvedServerId;
const navigationStatus = hasSession ? "ready" : "session_unavailable"; const navigationStatus = isRuntimeSessionAvailable
? "ready"
: "session_unavailable";
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
@@ -172,7 +180,7 @@ export function AgentReadyScreen({
}, [focusServerId, navigationStatus, resolvedAgentId]) }, [focusServerId, navigationStatus, resolvedAgentId])
); );
if (!hasSession || !resolvedServerId) { if (!resolvedServerId || !runtimeClient) {
return ( return (
<AgentSessionUnavailableState <AgentSessionUnavailableState
onBack={handleBackToHome} onBack={handleBackToHome}
@@ -189,6 +197,8 @@ export function AgentReadyScreen({
<AgentScreenContent <AgentScreenContent
serverId={resolvedServerId} serverId={resolvedServerId}
agentId={resolvedAgentId} agentId={resolvedAgentId}
client={runtimeClient}
isConnected={runtimeIsConnected}
connectionStatus={connectionStatus} connectionStatus={connectionStatus}
/> />
</ExplorerSidebarAnimationProvider> </ExplorerSidebarAnimationProvider>
@@ -198,7 +208,9 @@ export function AgentReadyScreen({
type AgentScreenContentProps = { type AgentScreenContentProps = {
serverId: string; serverId: string;
agentId?: string; agentId?: string;
connectionStatus: ConnectionStatus; client: DaemonClient;
isConnected: boolean;
connectionStatus: HostRuntimeConnectionStatus;
}; };
function toErrorMessage(error: unknown): string { function toErrorMessage(error: unknown): string {
@@ -215,6 +227,8 @@ function isNotFoundErrorMessage(message: string): boolean {
function AgentScreenContent({ function AgentScreenContent({
serverId, serverId,
agentId, agentId,
client,
isConnected,
connectionStatus, connectionStatus,
}: AgentScreenContentProps) { }: AgentScreenContentProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
@@ -465,16 +479,16 @@ function AgentScreenContent({
return filtered; return filtered;
}, [allPendingPermissions, resolvedAgentId]); }, [allPendingPermissions, resolvedAgentId]);
const client = useSessionStore( const hasSession = useSessionStore(
(state) => state.sessions[serverId]?.client ?? null (state) => Boolean(state.sessions[serverId])
);
const isConnected = useSessionStore(
(state) => state.sessions[serverId]?.connection.isConnected ?? false
); );
const focusedAgentId = useSessionStore( const focusedAgentId = useSessionStore(
(state) => state.sessions[serverId]?.focusedAgentId ?? null (state) => state.sessions[serverId]?.focusedAgentId ?? null
); );
const { ensureAgentIsInitialized, refreshAgent } = useAgentInitialization(serverId); const { ensureAgentIsInitialized, refreshAgent } = useAgentInitialization({
serverId,
client: hasSession ? client : null,
});
const [missingAgentState, setMissingAgentState] = useState<AgentScreenMissingState>({ const [missingAgentState, setMissingAgentState] = useState<AgentScreenMissingState>({
kind: "idle", kind: "idle",
}); });
@@ -529,7 +543,7 @@ function AgentScreenContent({
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
if (!resolvedAgentId || !isConnected) { if (!resolvedAgentId || !isConnected || !hasSession) {
return; return;
} }
ensureAgentIsInitialized(resolvedAgentId).catch((error) => { ensureAgentIsInitialized(resolvedAgentId).catch((error) => {
@@ -538,7 +552,7 @@ function AgentScreenContent({
error, error,
}); });
}); });
}, [ensureAgentIsInitialized, isConnected, resolvedAgentId]) }, [ensureAgentIsInitialized, hasSession, isConnected, resolvedAgentId])
); );
const isGitCheckout = activeExplorerCheckout?.isGit ?? false; const isGitCheckout = activeExplorerCheckout?.isGit ?? false;
@@ -745,7 +759,7 @@ function AgentScreenContent({
return; return;
} }
if (!isConnected) { if (!isConnected || !hasSession) {
return; return;
} }
// On native clients, daemon stream forwarding is focused-agent only, so switching // On native clients, daemon stream forwarding is focused-agent only, so switching
@@ -764,6 +778,7 @@ function AgentScreenContent({
}, [ }, [
resolvedAgentId, resolvedAgentId,
ensureAgentIsInitialized, ensureAgentIsInitialized,
hasSession,
isConnected, isConnected,
needsAuthoritativeSync, needsAuthoritativeSync,
]); ]);
@@ -784,7 +799,7 @@ function AgentScreenContent({
} }
return; return;
} }
if (!isConnected) { if (!isConnected || !hasSession) {
return; return;
} }
if (missingAgentState.kind === "resolving" || missingAgentState.kind === "not_found") { if (missingAgentState.kind === "resolving" || missingAgentState.kind === "not_found") {
@@ -855,6 +870,7 @@ function AgentScreenContent({
agent, agent,
client, client,
ensureAgentIsInitialized, ensureAgentIsInitialized,
hasSession,
isConnected, isConnected,
missingAgentState.kind, missingAgentState.kind,
resolvedAgentId, resolvedAgentId,
@@ -898,13 +914,13 @@ function AgentScreenContent({
]); ]);
const handleRefreshAgent = useCallback(() => { const handleRefreshAgent = useCallback(() => {
if (!resolvedAgentId) { if (!resolvedAgentId || !hasSession) {
return; return;
} }
void refreshAgent(resolvedAgentId).catch((error) => { void refreshAgent(resolvedAgentId).catch((error) => {
console.warn("[AgentScreen] refreshAgent failed", { agentId: resolvedAgentId, error }); console.warn("[AgentScreen] refreshAgent failed", { agentId: resolvedAgentId, error });
}); });
}, [resolvedAgentId, refreshAgent]); }, [hasSession, refreshAgent, resolvedAgentId]);
const handleCopyMeta = useCallback( const handleCopyMeta = useCallback(
async (label: string, value: string | null | undefined) => { async (label: string, value: string | null | undefined) => {
@@ -1262,7 +1278,7 @@ function AgentSessionUnavailableState({
}: { }: {
onBack: () => void; onBack: () => void;
serverLabel: string; serverLabel: string;
connectionStatus: ConnectionStatus; connectionStatus: HostRuntimeConnectionStatus;
lastError: string | null; lastError: string | null;
isUnknownDaemon?: boolean; isUnknownDaemon?: boolean;
}) { }) {
@@ -1285,19 +1301,24 @@ function AgentSessionUnavailableState({
} }
const isConnecting = connectionStatus === "connecting"; const isConnecting = connectionStatus === "connecting";
const isPreparingSession = connectionStatus === "online";
return ( return (
<View style={styles.container}> <View style={styles.container}>
<BackHeader title="Agent" onBack={onBack} /> <BackHeader title="Agent" onBack={onBack} />
<View style={styles.centerState}> <View style={styles.centerState}>
{isConnecting ? ( {isConnecting || isPreparingSession ? (
<> <>
<ActivityIndicator size="large" /> <ActivityIndicator size="large" />
<Text style={styles.loadingText}> <Text style={styles.loadingText}>
Connecting to {serverLabel}... {isPreparingSession
? `Preparing ${serverLabel} session...`
: `Connecting to ${serverLabel}...`}
</Text> </Text>
<Text style={styles.statusText}> <Text style={styles.statusText}>
We will show this agent once the host is online. {isPreparingSession
? "We will show this agent in a moment."
: "We will show this agent once the host is online."}
</Text> </Text>
</> </>
) : ( ) : (
@@ -35,6 +35,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 { useCreateFlowStore } from '@/stores/create-flow-store' import { useCreateFlowStore } from '@/stores/create-flow-store'
import { useHostRuntimeSession } 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'
@@ -368,18 +369,16 @@ function DraftAgentScreenContent({
return collectAgentWorkingDirectorySuggestions([...liveSources, ...fetchedSources]) return collectAgentWorkingDirectorySuggestions([...liveSources, ...fetchedSources])
}, [allAgents, sessionAgents]) }, [allAgents, sessionAgents])
const sessionClient = useSessionStore((state) => const { client: runtimeClient, isConnected: isHostOnline } = useHostRuntimeSession(
selectedServerId ? (state.sessions[selectedServerId]?.client ?? null) : null selectedServerId ?? ''
)
const isConnected = useSessionStore((state) =>
selectedServerId ? (state.sessions[selectedServerId]?.connection.isConnected ?? false) : false
) )
const sessionClient = runtimeClient
const trimmedWorkingDir = workingDir.trim() const trimmedWorkingDir = workingDir.trim()
const shouldInspectRepo = trimmedWorkingDir.length > 0 const shouldInspectRepo = trimmedWorkingDir.length > 0
const daemonAvailabilityError = const daemonAvailabilityError =
!selectedServerId || hostEntry?.status !== 'online' ? 'Host is offline' : null !selectedServerId || !isHostOnline ? 'Host is offline' : null
const repoAvailabilityError = const repoAvailabilityError =
shouldInspectRepo && (!hostEntry || hostEntry.status !== 'online' || !isConnected) shouldInspectRepo && !isHostOnline
? (daemonAvailabilityError ?? ? (daemonAvailabilityError ??
'Repository details will load automatically once the selected host is back online.') 'Repository details will load automatically once the selected host is back online.')
: null : null
@@ -398,7 +397,7 @@ function DraftAgentScreenContent({
Boolean(trimmedWorkingDir) && Boolean(trimmedWorkingDir) &&
!repoAvailabilityError && !repoAvailabilityError &&
Boolean(sessionClient) && Boolean(sessionClient) &&
isConnected, isHostOnline,
retry: false, retry: false,
staleTime: CHECKOUT_STATUS_STALE_TIME, staleTime: CHECKOUT_STATUS_STALE_TIME,
refetchOnMount: 'always', refetchOnMount: 'always',
@@ -461,7 +460,7 @@ function DraftAgentScreenContent({
Boolean(worktreeListRoot) && Boolean(worktreeListRoot) &&
!repoAvailabilityError && !repoAvailabilityError &&
Boolean(sessionClient) && Boolean(sessionClient) &&
isConnected && isHostOnline &&
!isNonGitDirectory, !isNonGitDirectory,
retry: false, retry: false,
staleTime: 0, staleTime: 0,
@@ -526,7 +525,7 @@ function DraftAgentScreenContent({
Boolean(trimmedWorkingDir) && Boolean(trimmedWorkingDir) &&
!repoAvailabilityError && !repoAvailabilityError &&
Boolean(sessionClient) && Boolean(sessionClient) &&
isConnected, isHostOnline,
retry: false, retry: false,
staleTime: 15_000, staleTime: 15_000,
}) })
@@ -559,7 +558,7 @@ function DraftAgentScreenContent({
Boolean(selectedServerId) && Boolean(selectedServerId) &&
!daemonAvailabilityError && !daemonAvailabilityError &&
Boolean(sessionClient) && Boolean(sessionClient) &&
isConnected, isHostOnline,
retry: false, retry: false,
staleTime: 15_000, staleTime: 15_000,
}) })
@@ -625,7 +624,7 @@ function DraftAgentScreenContent({
Boolean(baseBranch) && Boolean(baseBranch) &&
Boolean(trimmedWorkingDir) && Boolean(trimmedWorkingDir) &&
Boolean(sessionClient) && Boolean(sessionClient) &&
isConnected, isHostOnline,
retry: false, retry: false,
staleTime: 30_000, staleTime: 30_000,
}) })
@@ -810,9 +809,7 @@ function DraftAgentScreenContent({
}) })
}, [baseBranch, branchSearchQuery, branchSuggestionsQuery.data, checkout, worktreeOptions]) }, [baseBranch, branchSearchQuery, branchSuggestionsQuery.data, checkout, worktreeOptions])
const createAgentClient = useSessionStore((state) => const createAgentClient = sessionClient
selectedServerId ? (state.sessions[selectedServerId]?.client ?? null) : null
)
const draftCommandConfig = useMemo<DraftCommandConfig | undefined>(() => { const draftCommandConfig = useMemo<DraftCommandConfig | undefined>(() => {
const cwd = ( const cwd = (
isAttachWorktree && selectedWorktreePath ? selectedWorktreePath : workingDir isAttachWorktree && selectedWorktreePath ? selectedWorktreePath : workingDir
+5 -3
View File
@@ -22,6 +22,7 @@ import { measureConnectionLatency } from "@/utils/test-daemon-connection";
import { confirmDialog } from "@/utils/confirm-dialog"; import { confirmDialog } from "@/utils/confirm-dialog";
import { MenuHeader } from "@/components/headers/menu-header"; import { MenuHeader } from "@/components/headers/menu-header";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import { useHostRuntimeSession } 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";
import { PairLinkModal } from "@/components/pair-link-modal"; import { PairLinkModal } from "@/components/pair-link-modal";
@@ -1035,10 +1036,11 @@ function HostDetailModal({
); );
// Restart logic (moved from DaemonCard) // Restart logic (moved from DaemonCard)
const daemonClient = useSessionStore((state) => host ? (state.sessions[host.serverId]?.client ?? null) : null); const { client: runtimeClient, isConnected } = useHostRuntimeSession(
const daemonConnection = useSessionStore((state) => host ? (state.sessions[host.serverId]?.connection ?? null) : null); host?.serverId ?? ""
);
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);
const isConnected = daemonConnection?.isConnected ?? false;
const isConnectedRef = useRef(isConnected); const isConnectedRef = useRef(isConnected);
const [isRestarting, setIsRestarting] = useState(false); const [isRestarting, setIsRestarting] = useState(false);
-51
View File
@@ -147,12 +147,6 @@ export interface AgentFileExplorerState {
selectedEntryPath: string | null; selectedEntryPath: string | null;
} }
export interface DaemonConnectionSnapshot {
isConnected: boolean;
isConnecting: boolean;
lastError: string | null;
}
export type DaemonServerInfo = { export type DaemonServerInfo = {
serverId: string; serverId: string;
hostname: string | null; hostname: string | null;
@@ -173,9 +167,6 @@ export interface SessionState {
// Daemon client (immutable reference) // Daemon client (immutable reference)
client: DaemonClient | null; client: DaemonClient | null;
// Connection snapshot (mutable)
connection: DaemonConnectionSnapshot;
// Server metadata (from server_info handshake) // Server metadata (from server_info handshake)
serverInfo: DaemonServerInfo | null; serverInfo: DaemonServerInfo | null;
@@ -233,7 +224,6 @@ interface SessionStoreActions {
clearSession: (serverId: string) => void; clearSession: (serverId: string) => void;
getSession: (serverId: string) => SessionState | undefined; getSession: (serverId: string) => SessionState | undefined;
updateSessionClient: (serverId: string, client: DaemonClient) => void; updateSessionClient: (serverId: string, client: DaemonClient) => void;
updateSessionConnection: (serverId: string, connection: DaemonConnectionSnapshot) => void;
updateSessionServerInfo: (serverId: string, info: DaemonServerInfo) => void; updateSessionServerInfo: (serverId: string, info: DaemonServerInfo) => void;
// Audio state // Audio state
@@ -309,25 +299,11 @@ function logSessionStoreUpdate(
}); });
} }
function createDefaultConnectionSnapshot(client?: DaemonClient | null): DaemonConnectionSnapshot {
if (!client) {
return { isConnected: false, isConnecting: false, lastError: null };
}
const state = client.getConnectionState();
return {
isConnected: state.status === "connected",
isConnecting: state.status === "connecting",
lastError: state.status === "disconnected" ? state.reason ?? client.lastError ?? null : null,
};
}
// Helper to create initial session state // Helper to create initial session state
function createInitialSessionState(serverId: string, client: DaemonClient, audioPlayer: ReturnType<typeof useAudioPlayer>): SessionState { function createInitialSessionState(serverId: string, client: DaemonClient, audioPlayer: ReturnType<typeof useAudioPlayer>): SessionState {
return { return {
serverId, serverId,
client, client,
connection: createDefaultConnectionSnapshot(client),
serverInfo: null, serverInfo: null,
audioPlayer, audioPlayer,
hasHydratedAgents: false, hasHydratedAgents: false,
@@ -420,33 +396,6 @@ export const useSessionStore = create<SessionStore>()(
}); });
}, },
updateSessionConnection: (serverId, connection) => {
set((prev) => {
const session = prev.sessions[serverId];
if (!session) {
return prev;
}
if (
session.connection.isConnected === connection.isConnected &&
session.connection.isConnecting === connection.isConnecting &&
session.connection.lastError === connection.lastError
) {
return prev;
}
logSessionStoreUpdate("updateSessionConnection", serverId, connection);
return {
...prev,
sessions: {
...prev.sessions,
[serverId]: {
...session,
connection,
},
},
};
});
},
updateSessionServerInfo: (serverId, info) => { updateSessionServerInfo: (serverId, info) => {
set((prev) => { set((prev) => {
const session = prev.sessions[serverId]; const session = prev.sessions[serverId];