refactor: remove daemon connections context, use host runtime directly

This commit is contained in:
Mohamed Boudra
2026-02-21 08:52:35 +07:00
parent 0350f00840
commit 5dae7f854f
13 changed files with 117 additions and 181 deletions
+29 -32
View File
@@ -13,7 +13,6 @@ import { View, ActivityIndicator, Text } from "react-native";
import { UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import { UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { darkTheme } from "@/styles/theme"; import { darkTheme } from "@/styles/theme";
import { DaemonRegistryProvider, useDaemonRegistry } from "@/contexts/daemon-registry-context"; import { DaemonRegistryProvider, useDaemonRegistry } from "@/contexts/daemon-registry-context";
import { DaemonConnectionsProvider, useDaemonConnections } from "@/contexts/daemon-connections-context";
import { MultiDaemonSessionHost } from "@/components/multi-daemon-session-host"; import { MultiDaemonSessionHost } from "@/components/multi-daemon-session-host";
import { QueryClientProvider } from "@tanstack/react-query"; import { QueryClientProvider } from "@tanstack/react-query";
import { useState, useEffect, type ReactNode, useMemo, useRef } from "react"; import { useState, useEffect, type ReactNode, useMemo, useRef } from "react";
@@ -449,37 +448,35 @@ export default function RootLayout() {
<BottomSheetModalProvider> <BottomSheetModalProvider>
<QueryProvider> <QueryProvider>
<DaemonRegistryProvider> <DaemonRegistryProvider>
<DaemonConnectionsProvider> <PushNotificationRouter />
<PushNotificationRouter /> <MultiDaemonSessionHost />
<MultiDaemonSessionHost /> <ProvidersWrapper>
<ProvidersWrapper> <SidebarAnimationProvider>
<SidebarAnimationProvider> <HorizontalScrollProvider>
<HorizontalScrollProvider> <ToastProvider>
<ToastProvider> <AppWithSidebar>
<AppWithSidebar> <Stack
<Stack screenOptions={{
screenOptions={{ headerShown: false,
headerShown: false, animation: "none",
animation: "none", }}
}} >
> <Stack.Screen name="index" />
<Stack.Screen name="index" /> <Stack.Screen
<Stack.Screen name="h/[serverId]/agent/[agentId]"
name="h/[serverId]/agent/[agentId]" options={{ gestureEnabled: false }}
options={{ gestureEnabled: false }} />
/> <Stack.Screen name="h/[serverId]/index" />
<Stack.Screen name="h/[serverId]/index" /> <Stack.Screen name="h/[serverId]/agent/index" />
<Stack.Screen name="h/[serverId]/agent/index" /> <Stack.Screen name="h/[serverId]/agents" />
<Stack.Screen name="h/[serverId]/agents" /> <Stack.Screen name="h/[serverId]/settings" />
<Stack.Screen name="h/[serverId]/settings" /> <Stack.Screen name="pair-scan" />
<Stack.Screen name="pair-scan" /> </Stack>
</Stack> </AppWithSidebar>
</AppWithSidebar> </ToastProvider>
</ToastProvider> </HorizontalScrollProvider>
</HorizontalScrollProvider> </SidebarAnimationProvider>
</SidebarAnimationProvider> </ProvidersWrapper>
</ProvidersWrapper>
</DaemonConnectionsProvider>
</DaemonRegistryProvider> </DaemonRegistryProvider>
</QueryProvider> </QueryProvider>
</BottomSheetModalProvider> </BottomSheetModalProvider>
@@ -49,7 +49,7 @@ import type {
ExplorerEntry, ExplorerEntry,
ExplorerFile, ExplorerFile,
} from "@/stores/session-store"; } from "@/stores/session-store";
import { useDaemonConnections } from "@/contexts/daemon-connections-context"; import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import { useDownloadStore } from "@/stores/download-store"; import { useDownloadStore } from "@/stores/download-store";
import { import {
@@ -95,8 +95,11 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const showDesktopWebScrollbar = Platform.OS === "web" && !isMobile; const showDesktopWebScrollbar = Platform.OS === "web" && !isMobile;
const { connectionStates } = useDaemonConnections(); const { daemons } = useDaemonRegistry();
const daemonProfile = connectionStates.get(serverId)?.daemon; const daemonProfile = useMemo(
() => daemons.find((daemon) => daemon.serverId === serverId),
[daemons, serverId]
);
const agentExists = useSessionStore((state) => const agentExists = useSessionStore((state) =>
agentId && state.sessions[serverId] agentId && state.sessions[serverId]
? state.sessions[serverId]?.agents.has(agentId) ? state.sessions[serverId]?.agents.has(agentId)
+18 -6
View File
@@ -1,4 +1,11 @@
import { useCallback, useMemo, useState, useEffect, useRef } from "react"; import {
useCallback,
useMemo,
useState,
useEffect,
useRef,
useSyncExternalStore,
} from "react";
import { View, Pressable, Text, Platform } from "react-native"; import { View, Pressable, Text, Platform } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import Animated, { import Animated, {
@@ -20,7 +27,7 @@ import { useTauriDragHandlers, useTrafficLightPadding } from "@/utils/tauri-wind
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { Combobox } from "@/components/ui/combobox"; import { Combobox } from "@/components/ui/combobox";
import { useDaemonRegistry } from "@/contexts/daemon-registry-context"; import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
import { useDaemonConnections } from "@/contexts/daemon-connections-context"; import { getHostRuntimeStore } from "@/runtime/host-runtime";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import { formatConnectionStatus } from "@/utils/daemons"; import { formatConnectionStatus } from "@/utils/daemons";
import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE } from "@/constants/layout"; import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE } from "@/constants/layout";
@@ -57,7 +64,12 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
const closeToAgent = usePanelStore((state) => state.closeToAgent); const closeToAgent = usePanelStore((state) => state.closeToAgent);
const pathname = usePathname(); const pathname = usePathname();
const { daemons } = useDaemonRegistry(); const { daemons } = useDaemonRegistry();
const { connectionStates } = useDaemonConnections(); const runtime = getHostRuntimeStore();
const runtimeVersion = useSyncExternalStore(
(onStoreChange) => runtime.subscribeAll(onStoreChange),
() => runtime.getVersion(),
() => runtime.getVersion()
);
const activeServerIdFromPath = useMemo( const activeServerIdFromPath = useMemo(
() => parseServerIdFromPathname(pathname), () => parseServerIdFromPathname(pathname),
[pathname] [pathname]
@@ -70,7 +82,7 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
return trimmed && trimmed.length > 0 ? trimmed : activeServerId; return trimmed && trimmed.length > 0 ? trimmed : activeServerId;
}, [activeServerId, daemons]); }, [activeServerId, daemons]);
const activeHostStatus = activeServerId const activeHostStatus = activeServerId
? (connectionStates.get(activeServerId)?.status ?? "idle") ? runtime.getSnapshot(activeServerId)?.connectionStatus ?? "connecting"
: "idle"; : "idle";
const activeHostStatusColor = const activeHostStatusColor =
activeHostStatus === "online" activeHostStatus === "online"
@@ -84,10 +96,10 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
id: daemon.serverId, id: daemon.serverId,
label: daemon.label?.trim() || daemon.serverId, label: daemon.label?.trim() || daemon.serverId,
description: formatConnectionStatus( description: formatConnectionStatus(
connectionStates.get(daemon.serverId)?.status ?? "idle" runtime.getSnapshot(daemon.serverId)?.connectionStatus ?? "connecting"
), ),
})), })),
[connectionStates, daemons] [daemons, runtime, runtimeVersion]
); );
const hostTriggerRef = useRef<View>(null); const hostTriggerRef = useRef<View>(null);
const [isHostPickerOpen, setIsHostPickerOpen] = useState(false); const [isHostPickerOpen, setIsHostPickerOpen] = useState(false);
+5 -4
View File
@@ -3,11 +3,11 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { MicOff, Square } from "lucide-react-native"; import { MicOff, Square } from "lucide-react-native";
import { VolumeMeter } from "./volume-meter"; import { VolumeMeter } from "./volume-meter";
import { useVoice } from "@/contexts/voice-context"; import { useVoice } from "@/contexts/voice-context";
import { useDaemonConnections } from "@/contexts/daemon-connections-context"; import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
export function VoicePanel() { export function VoicePanel() {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const { connectionStates } = useDaemonConnections(); const { daemons } = useDaemonRegistry();
const { const {
volume, volume,
isMuted, isMuted,
@@ -18,8 +18,9 @@ export function VoicePanel() {
activeServerId, activeServerId,
} = useVoice(); } = useVoice();
const activeHost = activeServerId ? connectionStates.get(activeServerId) ?? null : null; const hostLabel = activeServerId
const hostLabel = activeHost?.daemon.label ?? null; ? daemons.find((daemon) => daemon.serverId === activeServerId)?.label ?? null
: null;
const hostSuffix = hostLabel ? ` (${hostLabel})` : ""; const hostSuffix = hostLabel ? ` (${hostLabel})` : "";
return ( return (
@@ -1,97 +0,0 @@
import { createContext, useContext, useMemo, useSyncExternalStore } from "react";
import type { ReactNode } from "react";
import {
getHostRuntimeStore,
type HostRuntimeAgentDirectoryStatus,
type HostRuntimeConnectionStatus,
} from "@/runtime/host-runtime";
import { useDaemonRegistry, type HostProfile } from "./daemon-registry-context";
export type ActiveConnection =
| { type: "direct"; endpoint: string; display: string }
| { type: "relay"; endpoint: string; display: "relay" };
export type ConnectionStatus = HostRuntimeConnectionStatus;
export type DaemonConnectionRecord = {
daemon: HostProfile;
status: HostRuntimeConnectionStatus;
activeConnection: ActiveConnection | null;
lastError: string | null;
lastOnlineAt: string | null;
agentDirectoryStatus: HostRuntimeAgentDirectoryStatus;
agentDirectoryError: string | null;
hasEverLoadedAgentDirectory: boolean;
};
interface DaemonConnectionsContextValue {
connectionStates: Map<string, DaemonConnectionRecord>;
isLoading: boolean;
}
const DaemonConnectionsContext = createContext<DaemonConnectionsContextValue | null>(null);
function buildConnectionStates(input: {
daemons: HostProfile[];
runtime: ReturnType<typeof getHostRuntimeStore>;
}): Map<string, DaemonConnectionRecord> {
const { daemons, runtime } = input;
const next = new Map<string, DaemonConnectionRecord>();
for (const daemon of daemons) {
const snapshot = runtime.getSnapshot(daemon.serverId);
next.set(daemon.serverId, {
daemon,
status: snapshot?.connectionStatus ?? "connecting",
activeConnection: snapshot?.activeConnection ?? null,
lastError: snapshot?.lastError ?? null,
lastOnlineAt: snapshot?.lastOnlineAt ?? null,
agentDirectoryStatus: snapshot?.agentDirectoryStatus ?? "initial_loading",
agentDirectoryError: snapshot?.agentDirectoryError ?? null,
hasEverLoadedAgentDirectory:
snapshot?.hasEverLoadedAgentDirectory ?? false,
});
}
return next;
}
export function useDaemonConnections(): DaemonConnectionsContextValue {
const ctx = useContext(DaemonConnectionsContext);
if (!ctx) {
throw new Error(
"useDaemonConnections must be used within DaemonConnectionsProvider"
);
}
return ctx;
}
export function DaemonConnectionsProvider({ children }: { children: ReactNode }) {
const { daemons, isLoading: registryLoading } = useDaemonRegistry();
const runtime = getHostRuntimeStore();
const runtimeVersion = useSyncExternalStore(
(onStoreChange) => runtime.subscribeAll(onStoreChange),
() => runtime.getVersion(),
() => runtime.getVersion()
);
const connectionStates = useMemo(
() => buildConnectionStates({ daemons, runtime }),
[daemons, runtime, runtimeVersion]
);
const value = useMemo<DaemonConnectionsContextValue>(
() => ({
connectionStates,
isLoading: registryLoading,
}),
[connectionStates, registryLoading]
);
return (
<DaemonConnectionsContext.Provider value={value}>
{children}
</DaemonConnectionsContext.Provider>
);
}
+19 -10
View File
@@ -1,6 +1,6 @@
import { useMemo, useCallback } from "react"; import { useMemo, useCallback, useSyncExternalStore } from "react";
import { useShallow } from "zustand/shallow"; import { useShallow } from "zustand/shallow";
import { useDaemonConnections } from "@/contexts/daemon-connections-context"; import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import type { AgentDirectoryEntry } from "@/types/agent-directory"; import type { AgentDirectoryEntry } from "@/types/agent-directory";
import type { Agent } from "@/stores/session-store"; import type { Agent } from "@/stores/session-store";
@@ -20,8 +20,13 @@ export interface AggregatedAgentsResult {
} }
export function useAggregatedAgents(): AggregatedAgentsResult { export function useAggregatedAgents(): AggregatedAgentsResult {
const { connectionStates } = useDaemonConnections(); const { daemons } = useDaemonRegistry();
const runtime = getHostRuntimeStore(); const runtime = getHostRuntimeStore();
const runtimeVersion = useSyncExternalStore(
(onStoreChange) => runtime.subscribeAll(onStoreChange),
() => runtime.getVersion(),
() => runtime.getVersion()
);
const sessionAgents = useSessionStore( const sessionAgents = useSessionStore(
useShallow((state) => { useShallow((state) => {
@@ -39,13 +44,16 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
const result = useMemo(() => { const result = useMemo(() => {
const allAgents: AggregatedAgent[] = []; const allAgents: AggregatedAgent[] = [];
const serverLabelById = new Map(
daemons.map((daemon) => [daemon.serverId, daemon.label] as const)
);
// Derive agent directory from all sessions // Derive agent directory from all sessions
for (const [serverId, agents] of Object.entries(sessionAgents)) { for (const [serverId, agents] of Object.entries(sessionAgents)) {
if (!agents || agents.size === 0) { if (!agents || agents.size === 0) {
continue; continue;
} }
const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId; const serverLabel = serverLabelById.get(serverId) ?? serverId;
for (const agent of agents.values()) { for (const agent of agents.values()) {
const nextAgent: AggregatedAgent = { const nextAgent: AggregatedAgent = {
id: agent.id, id: agent.id,
@@ -85,11 +93,12 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
const hasAnyData = allAgents.length > 0; const hasAnyData = allAgents.length > 0;
// Align list loading with the runtime directory-sync machine. // Align list loading with the runtime directory-sync machine.
const isLoading = Array.from(connectionStates.values()).some( const isLoading = daemons.some((daemon) => {
(connection) => const status =
connection.agentDirectoryStatus === "initial_loading" || runtime.getSnapshot(daemon.serverId)?.agentDirectoryStatus ??
connection.agentDirectoryStatus === "revalidating" "initial_loading";
); return status === "initial_loading" || status === "revalidating";
});
const isInitialLoad = isLoading && !hasAnyData; const isInitialLoad = isLoading && !hasAnyData;
const isRevalidating = isLoading && hasAnyData; const isRevalidating = isLoading && hasAnyData;
@@ -99,7 +108,7 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
isInitialLoad, isInitialLoad,
isRevalidating, isRevalidating,
}; };
}, [sessionAgents, connectionStates]); }, [daemons, runtime, runtimeVersion, sessionAgents]);
return { return {
...result, ...result,
@@ -1,5 +1,5 @@
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useDaemonConnections } from "@/contexts/daemon-connections-context"; import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
import { useSessionStore, type Agent } from "@/stores/session-store"; import { useSessionStore, type Agent } from "@/stores/session-store";
import { import {
getHostRuntimeStore, getHostRuntimeStore,
@@ -37,7 +37,7 @@ function toAggregatedAgent(params: {
export function useAllAgentsList(options?: { export function useAllAgentsList(options?: {
serverId?: string | null; serverId?: string | null;
}): AggregatedAgentsResult { }): AggregatedAgentsResult {
const { connectionStates } = useDaemonConnections(); const { daemons } = useDaemonRegistry();
const runtime = getHostRuntimeStore(); const runtime = getHostRuntimeStore();
const serverId = useMemo(() => { const serverId = useMemo(() => {
@@ -64,7 +64,8 @@ export function useAllAgentsList(options?: {
if (!serverId || !liveAgents) { if (!serverId || !liveAgents) {
return []; return [];
} }
const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId; const serverLabel =
daemons.find((daemon) => daemon.serverId === serverId)?.label ?? serverId;
const list: AggregatedAgent[] = []; const list: AggregatedAgent[] = [];
for (const agent of liveAgents.values()) { for (const agent of liveAgents.values()) {
@@ -95,7 +96,7 @@ export function useAllAgentsList(options?: {
}); });
return list; return list;
}, [connectionStates, liveAgents, serverId]); }, [daemons, liveAgents, serverId]);
const isDirectoryLoading = Boolean(serverId && isHostRuntimeDirectoryLoading(snapshot)); const isDirectoryLoading = Boolean(serverId && isHostRuntimeDirectoryLoading(snapshot));
const isInitialLoad = isDirectoryLoading && agents.length === 0; const isInitialLoad = isDirectoryLoading && agents.length === 0;
@@ -1,5 +1,5 @@
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useDaemonConnections } from "@/contexts/daemon-connections-context"; import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
import { useSessionStore, type Agent } from "@/stores/session-store"; import { useSessionStore, type Agent } from "@/stores/session-store";
import { import {
getHostRuntimeStore, getHostRuntimeStore,
@@ -149,7 +149,7 @@ export function useSidebarAgentsList(options?: {
serverId?: string | null; serverId?: string | null;
selectedProjectFilterKeys?: string[]; selectedProjectFilterKeys?: string[];
}): SidebarAgentsListResult { }): SidebarAgentsListResult {
const { connectionStates } = useDaemonConnections(); const { daemons } = useDaemonRegistry();
const runtime = getHostRuntimeStore(); const runtime = getHostRuntimeStore();
const serverId = useMemo(() => { const serverId = useMemo(() => {
const value = options?.serverId; const value = options?.serverId;
@@ -184,7 +184,8 @@ export function useSidebarAgentsList(options?: {
}; };
} }
const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId; const serverLabel =
daemons.find((daemon) => daemon.serverId === serverId)?.label ?? serverId;
const seenAgentIds = new Set<string>(); const seenAgentIds = new Set<string>();
const byProject = new Map<string, SidebarProjectFilterOption>(); const byProject = new Map<string, SidebarProjectFilterOption>();
const mergedEntries: SidebarAgentListEntry[] = []; const mergedEntries: SidebarAgentListEntry[] = [];
@@ -261,7 +262,7 @@ export function useSidebarAgentsList(options?: {
hasAnyData: ordered.entries.length > 0, hasAnyData: ordered.entries.length > 0,
hasMoreEntries: ordered.hasMore, hasMoreEntries: ordered.hasMore,
}; };
}, [connectionStates, liveAgents, selectedProjectFilterKeys, serverId]); }, [daemons, liveAgents, selectedProjectFilterKeys, serverId]);
const refreshAll = useCallback(() => { const refreshAll = useCallback(() => {
if (!serverId || snapshot?.connectionStatus !== "online") { if (!serverId || snapshot?.connectionStatus !== "online") {
+4 -1
View File
@@ -5,7 +5,6 @@ import {
type FetchAgentsOptions, type FetchAgentsOptions,
} from "@server/client/daemon-client"; } from "@server/client/daemon-client";
import type { HostConnection, HostProfile } from "@/contexts/daemon-registry-context"; import type { HostConnection, HostProfile } from "@/contexts/daemon-registry-context";
import type { ActiveConnection } from "@/contexts/daemon-connections-context";
import { import {
buildDaemonWebSocketUrl, buildDaemonWebSocketUrl,
buildRelayWebSocketUrl, buildRelayWebSocketUrl,
@@ -28,6 +27,10 @@ export type HostRuntimeConnectionStatus =
| "offline" | "offline"
| "error"; | "error";
export type ActiveConnection =
| { type: "direct"; endpoint: string; display: string }
| { type: "relay"; endpoint: string; display: "relay" };
export type HostRuntimeAgentDirectoryStatus = export type HostRuntimeAgentDirectoryStatus =
| "idle" | "idle"
| "initial_loading" | "initial_loading"
@@ -40,7 +40,7 @@ import {
ExplorerSidebarAnimationProvider, ExplorerSidebarAnimationProvider,
} 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 { useDaemonRegistry } from "@/contexts/daemon-registry-context";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import { import {
useHostRuntimeSession, useHostRuntimeSession,
@@ -112,7 +112,7 @@ export function AgentReadyScreen({
const router = useRouter(); const router = useRouter();
const resolvedAgentId = agentId?.trim() || undefined; const resolvedAgentId = agentId?.trim() || undefined;
const resolvedServerId = serverId?.trim() || undefined; const resolvedServerId = serverId?.trim() || undefined;
const { connectionStates } = useDaemonConnections(); const { daemons } = useDaemonRegistry();
const runtimeServerId = resolvedServerId ?? ""; const runtimeServerId = resolvedServerId ?? "";
const { const {
snapshot: runtimeSnapshot, snapshot: runtimeSnapshot,
@@ -121,12 +121,12 @@ export function AgentReadyScreen({
} = useHostRuntimeSession(runtimeServerId); } = useHostRuntimeSession(runtimeServerId);
const connectionServerId = resolvedServerId ?? null; const connectionServerId = resolvedServerId ?? null;
const connection = connectionServerId const daemon = connectionServerId
? connectionStates.get(connectionServerId) ? daemons.find((entry) => entry.serverId === connectionServerId) ?? null
: null; : null;
const serverLabel = const serverLabel =
connection?.daemon.label ?? connectionServerId ?? "Selected host"; daemon?.label ?? connectionServerId ?? "Selected host";
const isUnknownDaemon = Boolean(connectionServerId && !connection); const isUnknownDaemon = Boolean(connectionServerId && !daemon);
const connectionStatus: HostRuntimeConnectionStatus = const connectionStatus: HostRuntimeConnectionStatus =
runtimeSnapshot?.connectionStatus ?? runtimeSnapshot?.connectionStatus ??
(isUnknownDaemon ? "offline" : "connecting"); (isUnknownDaemon ? "offline" : "connecting");
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from 'react'
import { createNameId } from 'mnemonic-id' import { createNameId } from 'mnemonic-id'
import type { ImageAttachment } from '@/components/message-input' import type { ImageAttachment } from '@/components/message-input'
import { View, Text, Pressable, ScrollView, Keyboard, Platform } from 'react-native' import { View, Text, Pressable, ScrollView, Keyboard, Platform } from 'react-native'
@@ -26,7 +26,6 @@ import {
checkoutStatusQueryKey, checkoutStatusQueryKey,
} from '@/hooks/use-checkout-status-query' } from '@/hooks/use-checkout-status-query'
import { useAllAgentsList } from '@/hooks/use-all-agents-list' import { useAllAgentsList } from '@/hooks/use-all-agents-list'
import { useDaemonConnections } from '@/contexts/daemon-connections-context'
import { useDaemonRegistry } from '@/contexts/daemon-registry-context' import { useDaemonRegistry } from '@/contexts/daemon-registry-context'
import { buildBranchComboOptions, normalizeBranchOptionName } from '@/utils/branch-suggestions' import { buildBranchComboOptions, normalizeBranchOptionName } from '@/utils/branch-suggestions'
import { shortenPath } from '@/utils/shorten-path' import { shortenPath } from '@/utils/shorten-path'
@@ -35,7 +34,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 { getHostRuntimeStore, 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'
@@ -139,8 +138,13 @@ function DraftAgentScreenContent({
const { theme } = useUnistyles() const { theme } = useUnistyles()
const router = useRouter() const router = useRouter()
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const { connectionStates } = useDaemonConnections()
const { daemons } = useDaemonRegistry() const { daemons } = useDaemonRegistry()
const runtime = getHostRuntimeStore()
const runtimeVersion = useSyncExternalStore(
(onStoreChange) => runtime.subscribeAll(onStoreChange),
() => runtime.getVersion(),
() => runtime.getVersion()
)
const params = useLocalSearchParams<DraftAgentParams>() const params = useLocalSearchParams<DraftAgentParams>()
const { height: keyboardHeight } = useReanimatedKeyboardAnimation() const { height: keyboardHeight } = useReanimatedKeyboardAnimation()
@@ -174,11 +178,11 @@ function DraftAgentScreenContent({
if (daemons.length === 0) return [] if (daemons.length === 0) return []
const out: string[] = [] const out: string[] = []
for (const daemon of daemons) { for (const daemon of daemons) {
const status = connectionStates.get(daemon.serverId)?.status ?? 'idle' const status = runtime.getSnapshot(daemon.serverId)?.connectionStatus ?? 'connecting'
if (status === 'online') out.push(daemon.serverId) if (status === 'online') out.push(daemon.serverId)
} }
return out return out
}, [connectionStates, daemons]) }, [daemons, runtime, runtimeVersion])
const initialValues = useMemo((): CreateAgentInitialValues => { const initialValues = useMemo((): CreateAgentInitialValues => {
const values: CreateAgentInitialValues = {} const values: CreateAgentInitialValues = {}
@@ -228,7 +232,6 @@ function DraftAgentScreenContent({
isCreateFlow: true, isCreateFlow: true,
onlineServerIds, onlineServerIds,
}) })
const hostEntry = selectedServerId ? connectionStates.get(selectedServerId) : undefined
const isMobile = UnistylesRuntime.breakpoint === 'xs' || UnistylesRuntime.breakpoint === 'sm' const isMobile = UnistylesRuntime.breakpoint === 'xs' || UnistylesRuntime.breakpoint === 'sm'
const mobileView = usePanelStore((state) => state.mobileView) const mobileView = usePanelStore((state) => state.mobileView)
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen) const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen)
@@ -5,7 +5,7 @@ import { useRouter } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { BackHeader } from "@/components/headers/back-header"; import { BackHeader } from "@/components/headers/back-header";
import { useSessionDirectory } from "@/hooks/use-session-directory"; import { useSessionDirectory } from "@/hooks/use-session-directory";
import { useDaemonConnections } from "@/contexts/daemon-connections-context"; import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
import type { Agent } from "@/contexts/session-context"; import type { Agent } from "@/contexts/session-context";
import { import {
buildHostAgentDetailRoute, buildHostAgentDetailRoute,
@@ -21,7 +21,7 @@ type AgentMatch = {
export function LegacyAgentIdScreen({ agentId }: { agentId: string }) { export function LegacyAgentIdScreen({ agentId }: { agentId: string }) {
const router = useRouter(); const router = useRouter();
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const { connectionStates } = useDaemonConnections(); const { daemons } = useDaemonRegistry();
const sessionDirectory = useSessionDirectory(); const sessionDirectory = useSessionDirectory();
const resolvedAgentId = typeof agentId === "string" ? agentId.trim() : undefined; const resolvedAgentId = typeof agentId === "string" ? agentId.trim() : undefined;
@@ -39,12 +39,13 @@ export function LegacyAgentIdScreen({ agentId }: { agentId: string }) {
if (!agent) { if (!agent) {
return; return;
} }
const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId; const serverLabel =
daemons.find((daemon) => daemon.serverId === serverId)?.label ?? serverId;
results.push({ serverId, serverLabel, agent }); results.push({ serverId, serverLabel, agent });
}); });
return results; return results;
}, [resolvedAgentId, sessionDirectory, connectionStates]); }, [daemons, resolvedAgentId, sessionDirectory]);
const hasSessions = sessionDirectory.size > 0; const hasSessions = sessionDirectory.size > 0;
const isRedirecting = Boolean(resolvedAgentId && matches.length === 1); const isRedirecting = Boolean(resolvedAgentId && matches.length === 1);
+5 -3
View File
@@ -1,7 +1,7 @@
import type { ConnectionStatus } from "@/contexts/daemon-connections-context"; import type { HostRuntimeConnectionStatus } from "@/runtime/host-runtime";
import { assertUnreachable } from "./exhaustive"; import { assertUnreachable } from "./exhaustive";
export function formatConnectionStatus(status: ConnectionStatus): string { export function formatConnectionStatus(status: HostRuntimeConnectionStatus): string {
switch (status) { switch (status) {
case "online": case "online":
return "Online"; return "Online";
@@ -20,7 +20,9 @@ export function formatConnectionStatus(status: ConnectionStatus): string {
export type ConnectionStatusTone = "success" | "warning" | "error" | "muted"; export type ConnectionStatusTone = "success" | "warning" | "error" | "muted";
export function getConnectionStatusTone(status: ConnectionStatus): ConnectionStatusTone { export function getConnectionStatusTone(
status: HostRuntimeConnectionStatus
): ConnectionStatusTone {
switch (status) { switch (status) {
case "online": case "online":
return "success"; return "success";