feat: implement daemon connectivity updates and relay protocol versioning
This commit is contained in:
@@ -76,9 +76,10 @@ import {
|
||||
derivePendingPermissionKey,
|
||||
normalizeAgentSnapshot,
|
||||
} from "@/utils/agent-snapshots";
|
||||
import { resolveProjectPlacement } from "@/utils/project-placement";
|
||||
import { mergePendingCreateImages } from "@/utils/pending-create-images";
|
||||
import { shouldClearAgentAttentionOnView } from "@/utils/agent-attention";
|
||||
import type { DaemonClient, FetchAgentsEntry } from "@server/client/daemon-client";
|
||||
import type { DaemonClient } from "@server/client/daemon-client";
|
||||
import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -128,7 +129,7 @@ export function AgentReadyScreen({
|
||||
const isUnknownDaemon = Boolean(connectionServerId && !connection);
|
||||
const connectionStatus: HostRuntimeConnectionStatus =
|
||||
runtimeSnapshot?.connectionStatus ??
|
||||
(isUnknownDaemon ? "offline" : "idle");
|
||||
(isUnknownDaemon ? "offline" : "connecting");
|
||||
const lastConnectionError = runtimeSnapshot?.lastError ?? null;
|
||||
const isRuntimeSessionAvailable = Boolean(resolvedServerId && runtimeClient);
|
||||
|
||||
@@ -278,7 +279,6 @@ function AgentScreenContent({
|
||||
const checkout = checkoutStatusQuery.status;
|
||||
const resolveCachedCheckoutIsGit = useCallback(
|
||||
(params: {
|
||||
agentId?: string | null;
|
||||
cwd?: string | null;
|
||||
projectPlacementIsGit?: boolean;
|
||||
checkoutStatusIsGit?: boolean;
|
||||
@@ -287,19 +287,6 @@ function AgentScreenContent({
|
||||
return params.projectPlacementIsGit;
|
||||
}
|
||||
|
||||
const agentId = params.agentId?.trim();
|
||||
if (agentId) {
|
||||
const sidebarAgents = queryClient.getQueryData<{
|
||||
entries: FetchAgentsEntry[];
|
||||
}>(["sidebarAgentsList", serverId]);
|
||||
const sidebarIsGit = sidebarAgents?.entries.find(
|
||||
(entry) => entry.agent.id === agentId
|
||||
)?.project?.checkout?.isGit;
|
||||
if (typeof sidebarIsGit === "boolean") {
|
||||
return sidebarIsGit;
|
||||
}
|
||||
}
|
||||
|
||||
const cwd = params.cwd?.trim();
|
||||
if (!cwd) {
|
||||
return null;
|
||||
@@ -327,7 +314,6 @@ function AgentScreenContent({
|
||||
?.agents?.get(resolvedAgentId);
|
||||
const cwd = currentAgent?.cwd?.trim();
|
||||
const isGit = resolveCachedCheckoutIsGit({
|
||||
agentId: resolvedAgentId,
|
||||
cwd,
|
||||
projectPlacementIsGit: currentAgent?.projectPlacement?.checkout?.isGit,
|
||||
checkoutStatusIsGit: checkout?.isGit,
|
||||
@@ -394,7 +380,6 @@ function AgentScreenContent({
|
||||
const activeExplorerCheckout = useMemo<ExplorerCheckoutContext | null>(() => {
|
||||
const cwd = agent?.cwd?.trim();
|
||||
const isGit = resolveCachedCheckoutIsGit({
|
||||
agentId: resolvedAgentId,
|
||||
cwd,
|
||||
projectPlacementIsGit: agent?.projectPlacement?.checkout?.isGit,
|
||||
checkoutStatusIsGit: checkout?.isGit,
|
||||
@@ -458,6 +443,7 @@ function AgentScreenContent({
|
||||
? state.sessions[serverId]?.agentHistorySyncGeneration?.get(resolvedAgentId) ?? -1
|
||||
: -1
|
||||
);
|
||||
const hasHydratedHistoryBefore = agentHistorySyncGeneration >= 0;
|
||||
|
||||
// Select raw pending permissions - filter with useMemo to avoid new Map on every render
|
||||
const allPendingPermissions = useSessionStore(
|
||||
@@ -517,6 +503,38 @@ function AgentScreenContent({
|
||||
};
|
||||
});
|
||||
|
||||
const handleHistorySyncFailure = useCallback(
|
||||
({ origin, error }: { origin: "focus" | "entry"; error: unknown }) => {
|
||||
if (resolvedAgentId) {
|
||||
console.warn("[AgentScreen] history sync failed", {
|
||||
origin,
|
||||
agentId: resolvedAgentId,
|
||||
error,
|
||||
});
|
||||
}
|
||||
const message = toErrorMessage(error);
|
||||
setMissingAgentState((prev) => {
|
||||
if (prev.kind === "error" && prev.message === message) {
|
||||
return prev;
|
||||
}
|
||||
return { kind: "error", message };
|
||||
});
|
||||
},
|
||||
[resolvedAgentId]
|
||||
);
|
||||
|
||||
const ensureInitializedWithSyncErrorHandling = useCallback(
|
||||
(origin: "focus" | "entry") => {
|
||||
if (!resolvedAgentId) {
|
||||
return;
|
||||
}
|
||||
ensureAgentIsInitialized(resolvedAgentId).catch((error) => {
|
||||
handleHistorySyncFailure({ origin, error });
|
||||
});
|
||||
},
|
||||
[ensureAgentIsInitialized, handleHistorySyncFailure, resolvedAgentId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionStatus === "online") {
|
||||
reconnectToastArmedRef.current = false;
|
||||
@@ -534,25 +552,18 @@ function AgentScreenContent({
|
||||
}
|
||||
}, [connectionStatus, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (missingAgentState.kind !== "error") {
|
||||
return;
|
||||
}
|
||||
toast.error("Failed to refresh agent. Retrying in background.");
|
||||
}, [missingAgentState.kind, toast]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (!resolvedAgentId || !isConnected || !hasSession) {
|
||||
return;
|
||||
}
|
||||
ensureAgentIsInitialized(resolvedAgentId).catch((error) => {
|
||||
console.warn("[AgentScreen] focus sync failed", {
|
||||
agentId: resolvedAgentId,
|
||||
error,
|
||||
});
|
||||
});
|
||||
}, [ensureAgentIsInitialized, hasSession, isConnected, resolvedAgentId])
|
||||
ensureInitializedWithSyncErrorHandling("focus");
|
||||
}, [
|
||||
ensureInitializedWithSyncErrorHandling,
|
||||
hasSession,
|
||||
isConnected,
|
||||
resolvedAgentId,
|
||||
])
|
||||
);
|
||||
|
||||
const isGitCheckout = activeExplorerCheckout?.isGit ?? false;
|
||||
@@ -688,6 +699,7 @@ function AgentScreenContent({
|
||||
isHistorySyncing,
|
||||
needsAuthoritativeSync,
|
||||
shouldUseOptimisticStream,
|
||||
hasHydratedHistoryBefore,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -769,15 +781,10 @@ function AgentScreenContent({
|
||||
return;
|
||||
}
|
||||
|
||||
ensureAgentIsInitialized(resolvedAgentId).catch((error) => {
|
||||
console.warn("[AgentScreen] Agent initialization failed", {
|
||||
agentId: resolvedAgentId,
|
||||
error,
|
||||
});
|
||||
});
|
||||
ensureInitializedWithSyncErrorHandling("entry");
|
||||
}, [
|
||||
resolvedAgentId,
|
||||
ensureAgentIsInitialized,
|
||||
ensureInitializedWithSyncErrorHandling,
|
||||
hasSession,
|
||||
isConnected,
|
||||
needsAuthoritativeSync,
|
||||
@@ -831,21 +838,28 @@ function AgentScreenContent({
|
||||
return;
|
||||
}
|
||||
const normalized = normalizeAgentSnapshot(snapshot, serverId);
|
||||
const hydrated = {
|
||||
...normalized,
|
||||
projectPlacement: resolveProjectPlacement({
|
||||
projectPlacement: null,
|
||||
cwd: normalized.cwd,
|
||||
}),
|
||||
};
|
||||
setAgents(serverId, (prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(normalized.id, normalized);
|
||||
next.set(hydrated.id, hydrated);
|
||||
return next;
|
||||
});
|
||||
setPendingPermissions(serverId, (prev) => {
|
||||
const next = new Map(prev);
|
||||
for (const [key, pending] of next.entries()) {
|
||||
if (pending.agentId === normalized.id) {
|
||||
if (pending.agentId === hydrated.id) {
|
||||
next.delete(key);
|
||||
}
|
||||
}
|
||||
for (const request of normalized.pendingPermissions) {
|
||||
const key = derivePendingPermissionKey(normalized.id, request);
|
||||
next.set(key, { key, agentId: normalized.id, request });
|
||||
for (const request of hydrated.pendingPermissions) {
|
||||
const key = derivePendingPermissionKey(hydrated.id, request);
|
||||
next.set(key, { key, agentId: hydrated.id, request });
|
||||
}
|
||||
return next;
|
||||
});
|
||||
@@ -940,14 +954,31 @@ function AgentScreenContent({
|
||||
[theme.colors.primary, toast]
|
||||
);
|
||||
|
||||
const syncBannerMessage =
|
||||
viewState.tag !== "ready"
|
||||
? null
|
||||
: viewState.syncStatus === "catching_up"
|
||||
? "Refreshing agent history..."
|
||||
: viewState.syncStatus === "sync_error"
|
||||
? "Refresh delayed. Retrying in background."
|
||||
: null;
|
||||
const shouldEmitHistoryRefreshToast =
|
||||
viewState.tag === "ready" &&
|
||||
viewState.sync.status === "catching_up" &&
|
||||
viewState.sync.shouldEmitHistoryRefreshToast;
|
||||
const shouldEmitSyncErrorToast =
|
||||
viewState.tag === "ready" &&
|
||||
viewState.sync.status === "sync_error" &&
|
||||
viewState.sync.shouldEmitSyncErrorToast;
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldEmitHistoryRefreshToast) {
|
||||
return;
|
||||
}
|
||||
toast.show("Refreshing agent history...", {
|
||||
durationMs: 2200,
|
||||
testID: "agent-history-refresh-toast",
|
||||
});
|
||||
}, [shouldEmitHistoryRefreshToast, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldEmitSyncErrorToast) {
|
||||
return;
|
||||
}
|
||||
toast.error("Failed to refresh agent. Retrying in background.");
|
||||
}, [shouldEmitSyncErrorToast, toast]);
|
||||
|
||||
if (viewState.tag === "not_found") {
|
||||
return (
|
||||
@@ -1188,12 +1219,6 @@ function AgentScreenContent({
|
||||
}
|
||||
/>
|
||||
|
||||
{syncBannerMessage ? (
|
||||
<View style={styles.syncBanner} testID="agent-sync-banner">
|
||||
<Text style={styles.syncBannerText}>{syncBannerMessage}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Content Area with Keyboard Animation */}
|
||||
<View style={styles.contentContainer}>
|
||||
<ReanimatedAnimated.View
|
||||
@@ -1221,6 +1246,14 @@ function AgentScreenContent({
|
||||
/>
|
||||
)}
|
||||
|
||||
{viewState.tag === "ready" &&
|
||||
viewState.sync.status === "catching_up" &&
|
||||
viewState.sync.ui === "overlay" ? (
|
||||
<View style={styles.historySyncOverlay} testID="agent-history-overlay">
|
||||
<ActivityIndicator size="large" color={theme.colors.foregroundMuted} />
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
</View>
|
||||
</FileDropZone>
|
||||
|
||||
@@ -1357,24 +1390,20 @@ const styles = StyleSheet.create((theme) => ({
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
},
|
||||
syncBanner: {
|
||||
marginHorizontal: theme.spacing[4],
|
||||
marginTop: theme.spacing[1],
|
||||
marginBottom: theme.spacing[2],
|
||||
borderRadius: theme.borderRadius.full,
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.surface1,
|
||||
},
|
||||
syncBannerText: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
historySyncOverlay: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 40,
|
||||
},
|
||||
archivingOverlay: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
|
||||
@@ -12,17 +12,18 @@ import { router, useLocalSearchParams } from "expo-router";
|
||||
import Constants from "expo-constants";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||
import { useQueries } from "@tanstack/react-query";
|
||||
import { Sun, Moon, Monitor, Globe, Settings, RotateCw, Trash2, Check } from "lucide-react-native";
|
||||
import { useAppSettings, type AppSettings } from "@/hooks/use-settings";
|
||||
import { useDaemonRegistry, type HostProfile, type HostConnection } from "@/contexts/daemon-registry-context";
|
||||
import { useDaemonConnections, type ActiveConnection, type ConnectionStatus } from "@/contexts/daemon-connections-context";
|
||||
import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons";
|
||||
import { measureConnectionLatency } from "@/utils/test-daemon-connection";
|
||||
import { confirmDialog } from "@/utils/confirm-dialog";
|
||||
import { MenuHeader } from "@/components/headers/menu-header";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { useHostRuntimeSession } from "@/runtime/host-runtime";
|
||||
import {
|
||||
getHostRuntimeStore,
|
||||
isHostRuntimeConnected,
|
||||
useHostRuntimeSession,
|
||||
} from "@/runtime/host-runtime";
|
||||
import { AddHostMethodModal } from "@/components/add-host-method-modal";
|
||||
import { AddHostModal } from "@/components/add-host-modal";
|
||||
import { PairLinkModal } from "@/components/pair-link-modal";
|
||||
@@ -457,7 +458,6 @@ export default function SettingsScreen() {
|
||||
removeHost,
|
||||
removeConnection,
|
||||
} = useDaemonRegistry();
|
||||
const { connectionStates } = useDaemonConnections();
|
||||
const [isAddHostMethodVisible, setIsAddHostMethodVisible] = useState(false);
|
||||
const [isDirectHostVisible, setIsDirectHostVisible] = useState(false);
|
||||
const [isPasteLinkVisible, setIsPasteLinkVisible] = useState(false);
|
||||
@@ -723,17 +723,10 @@ export default function SettingsScreen() {
|
||||
</View>
|
||||
) : (
|
||||
daemons.map((daemon) => {
|
||||
const connection = connectionStates.get(daemon.serverId);
|
||||
const connectionStatus = connection?.status ?? "idle";
|
||||
const activeConnection = connection?.activeConnection ?? null;
|
||||
const lastConnectionError = connection?.lastError ?? null;
|
||||
return (
|
||||
<DaemonCard
|
||||
key={daemon.serverId}
|
||||
daemon={daemon}
|
||||
connectionStatus={connectionStatus}
|
||||
activeConnection={activeConnection}
|
||||
lastError={lastConnectionError}
|
||||
onOpenSettings={handleEditDaemon}
|
||||
/>
|
||||
);
|
||||
@@ -865,9 +858,6 @@ export default function SettingsScreen() {
|
||||
<HostDetailModal
|
||||
visible={Boolean(editingDaemonLive)}
|
||||
host={editingDaemonLive}
|
||||
connectionStatus={editingServerId ? (connectionStates.get(editingServerId)?.status ?? "idle") : "idle"}
|
||||
activeConnection={editingServerId ? (connectionStates.get(editingServerId)?.activeConnection ?? null) : null}
|
||||
lastError={editingServerId ? (connectionStates.get(editingServerId)?.lastError ?? null) : null}
|
||||
isSaving={isSavingEdit}
|
||||
onClose={handleCloseEditDaemon}
|
||||
onSave={(label) => void handleSaveEditDaemon(label)}
|
||||
@@ -983,9 +973,6 @@ export default function SettingsScreen() {
|
||||
interface HostDetailModalProps {
|
||||
visible: boolean;
|
||||
host: HostProfile | null;
|
||||
connectionStatus: ConnectionStatus;
|
||||
activeConnection: ActiveConnection | null;
|
||||
lastError: string | null;
|
||||
isSaving: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (label: string) => void;
|
||||
@@ -1000,9 +987,6 @@ interface HostDetailModalProps {
|
||||
function HostDetailModal({
|
||||
visible,
|
||||
host,
|
||||
connectionStatus,
|
||||
activeConnection,
|
||||
lastError,
|
||||
isSaving,
|
||||
onClose,
|
||||
onSave,
|
||||
@@ -1018,45 +1002,37 @@ function HostDetailModal({
|
||||
const [pendingRemoveConnection, setPendingRemoveConnection] = useState<{ serverId: string; connectionId: string; title: string } | null>(null);
|
||||
const [isRemovingConnection, setIsRemovingConnection] = useState(false);
|
||||
|
||||
// Latency probes for each connection
|
||||
// Read per-connection probes from host runtime snapshots.
|
||||
const connections = host?.connections ?? [];
|
||||
const latencyQueries = useQueries({
|
||||
queries: connections.map((conn) => ({
|
||||
queryKey: ["connection-latency", conn.id],
|
||||
queryFn: () => measureConnectionLatency(conn, { serverId: host?.serverId }),
|
||||
enabled: visible,
|
||||
refetchInterval: 5_000,
|
||||
staleTime: 4_000,
|
||||
gcTime: 60_000,
|
||||
retry: 1,
|
||||
})),
|
||||
});
|
||||
const latencyByConnectionId = new Map(
|
||||
connections.map((conn, i) => [conn.id, latencyQueries[i]] as const)
|
||||
);
|
||||
|
||||
// Restart logic (moved from DaemonCard)
|
||||
const { client: runtimeClient, isConnected } = useHostRuntimeSession(
|
||||
const { snapshot: runtimeSnapshot, client: runtimeClient, isConnected } = useHostRuntimeSession(
|
||||
host?.serverId ?? ""
|
||||
);
|
||||
const runtime = getHostRuntimeStore();
|
||||
const daemonClient = runtimeClient;
|
||||
const daemonVersion = useSessionStore((state) => host ? (state.sessions[host.serverId]?.serverInfo?.version ?? null) : null);
|
||||
const isConnectedRef = useRef(isConnected);
|
||||
const probeByConnectionId = runtimeSnapshot?.probeByConnectionId ?? new Map();
|
||||
const connectionStatus = runtimeSnapshot?.connectionStatus ?? "connecting";
|
||||
const activeConnection = runtimeSnapshot?.activeConnection ?? null;
|
||||
const lastError = runtimeSnapshot?.lastError ?? null;
|
||||
const [isRestarting, setIsRestarting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
isConnectedRef.current = isConnected;
|
||||
}, [isConnected]);
|
||||
const isHostConnected = useCallback(() => {
|
||||
if (!host) {
|
||||
return false;
|
||||
}
|
||||
return isHostRuntimeConnected(runtime.getSnapshot(host.serverId));
|
||||
}, [host, runtime]);
|
||||
|
||||
const waitForDaemonRestart = useCallback(async () => {
|
||||
const disconnectTimeoutMs = 7000;
|
||||
const reconnectTimeoutMs = 30000;
|
||||
|
||||
if (isConnectedRef.current) {
|
||||
await waitForCondition(() => !isConnectedRef.current, disconnectTimeoutMs);
|
||||
if (isHostConnected()) {
|
||||
await waitForCondition(() => !isHostConnected(), disconnectTimeoutMs);
|
||||
}
|
||||
|
||||
const reconnected = await waitForCondition(() => isConnectedRef.current, reconnectTimeoutMs);
|
||||
const reconnected = await waitForCondition(() => isHostConnected(), reconnectTimeoutMs);
|
||||
|
||||
if (isScreenMountedRef.current) {
|
||||
setIsRestarting(false);
|
||||
@@ -1067,12 +1043,12 @@ function HostDetailModal({
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [host, isScreenMountedRef, waitForCondition]);
|
||||
}, [host, isHostConnected, isScreenMountedRef, waitForCondition]);
|
||||
|
||||
const beginServerRestart = useCallback(() => {
|
||||
if (!daemonClient || !host) return;
|
||||
|
||||
if (!isConnectedRef.current) {
|
||||
if (!isHostConnected()) {
|
||||
Alert.alert(
|
||||
"Host offline",
|
||||
"This host is offline. Paseo reconnects automatically—wait until it's back online before restarting."
|
||||
@@ -1094,7 +1070,7 @@ function HostDetailModal({
|
||||
});
|
||||
|
||||
void waitForDaemonRestart();
|
||||
}, [daemonClient, host, isScreenMountedRef, waitForDaemonRestart]);
|
||||
}, [daemonClient, host, isHostConnected, isScreenMountedRef, waitForDaemonRestart]);
|
||||
|
||||
const handleRestartPress = useCallback(() => {
|
||||
if (!daemonClient || !host) {
|
||||
@@ -1225,14 +1201,14 @@ function HostDetailModal({
|
||||
<Text style={styles.label}>Connections</Text>
|
||||
<View style={{ gap: 8 }}>
|
||||
{host.connections.map((conn) => {
|
||||
const latency = latencyByConnectionId.get(conn.id);
|
||||
const probe = probeByConnectionId.get(conn.id);
|
||||
return (
|
||||
<ConnectionRow
|
||||
key={conn.id}
|
||||
connection={conn}
|
||||
latencyMs={latency?.data ?? undefined}
|
||||
latencyLoading={latency?.isLoading ?? false}
|
||||
latencyError={latency?.isError ?? false}
|
||||
latencyMs={probe?.status === "available" ? probe.latencyMs : undefined}
|
||||
latencyLoading={!probe || probe.status === "pending"}
|
||||
latencyError={probe?.status === "unavailable"}
|
||||
onRemove={() => {
|
||||
const title =
|
||||
conn.type === "relay"
|
||||
@@ -1272,7 +1248,7 @@ function HostDetailModal({
|
||||
leading={<RotateCw size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
|
||||
status={isRestarting ? "pending" : "idle"}
|
||||
pendingLabel="Restarting..."
|
||||
disabled={!daemonClient || !isConnectedRef.current}
|
||||
disabled={!daemonClient || !isConnected}
|
||||
>
|
||||
Restart daemon
|
||||
</DropdownMenuItem>
|
||||
@@ -1475,20 +1451,18 @@ function ConnectionRow({
|
||||
|
||||
interface DaemonCardProps {
|
||||
daemon: HostProfile;
|
||||
connectionStatus: ConnectionStatus;
|
||||
activeConnection: ActiveConnection | null;
|
||||
lastError: string | null;
|
||||
onOpenSettings: (daemon: HostProfile) => void;
|
||||
}
|
||||
|
||||
function DaemonCard({
|
||||
daemon,
|
||||
connectionStatus,
|
||||
activeConnection,
|
||||
lastError,
|
||||
onOpenSettings,
|
||||
}: DaemonCardProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const { snapshot } = useHostRuntimeSession(daemon.serverId);
|
||||
const connectionStatus = snapshot?.connectionStatus ?? "connecting";
|
||||
const activeConnection = snapshot?.activeConnection ?? null;
|
||||
const lastError = snapshot?.lastError ?? null;
|
||||
const daemonVersion = useSessionStore(
|
||||
useCallback(
|
||||
(state) => state.sessions[daemon.serverId]?.serverInfo?.version ?? null,
|
||||
|
||||
Reference in New Issue
Block a user