Fix provider snapshot session hydration and agent scoping
This commit is contained in:
@@ -887,26 +887,17 @@ export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStat
|
|||||||
const models = snapshotModels;
|
const models = snapshotModels;
|
||||||
|
|
||||||
const agentProviderDefinitions = useMemo(() => {
|
const agentProviderDefinitions = useMemo(() => {
|
||||||
if (snapshotEntries) {
|
|
||||||
return AGENT_PROVIDER_DEFINITIONS.filter((d) =>
|
|
||||||
snapshotEntries.some((e) => e.provider === d.id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const definition = AGENT_PROVIDER_DEFINITIONS.find((d) => d.id === agent?.provider);
|
const definition = AGENT_PROVIDER_DEFINITIONS.find((d) => d.id === agent?.provider);
|
||||||
return definition ? [definition] : [];
|
return definition ? [definition] : [];
|
||||||
}, [agent?.provider, snapshotEntries]);
|
}, [agent?.provider]);
|
||||||
|
|
||||||
const agentProviderModels = useMemo(() => {
|
const agentProviderModels = useMemo(() => {
|
||||||
const map = new Map<string, AgentModelDefinition[]>();
|
const map = new Map<string, AgentModelDefinition[]>();
|
||||||
if (snapshotEntries) {
|
if (agent?.provider && snapshotModels) {
|
||||||
for (const entry of snapshotEntries) {
|
map.set(agent.provider, snapshotModels);
|
||||||
if (entry.models) {
|
|
||||||
map.set(entry.provider, entry.models);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, [snapshotEntries]);
|
}, [agent?.provider, snapshotModels]);
|
||||||
|
|
||||||
const displayMode =
|
const displayMode =
|
||||||
availableModes.find((mode) => mode.id === agent?.currentModeId)?.label ||
|
availableModes.find((mode) => mode.id === agent?.currentModeId)?.label ||
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||||||
import { useClientActivity } from "@/hooks/use-client-activity";
|
import { useClientActivity } from "@/hooks/use-client-activity";
|
||||||
import { usePushTokenRegistration } from "@/hooks/use-push-token-registration";
|
import { usePushTokenRegistration } from "@/hooks/use-push-token-registration";
|
||||||
import { clearArchiveAgentPending } from "@/hooks/use-archive-agent";
|
import { clearArchiveAgentPending } from "@/hooks/use-archive-agent";
|
||||||
|
import { prefetchProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
||||||
import { generateMessageId, type StreamItem } from "@/types/stream";
|
import { generateMessageId, type StreamItem } from "@/types/stream";
|
||||||
import {
|
import {
|
||||||
processTimelineResponse,
|
processTimelineResponse,
|
||||||
@@ -603,6 +604,34 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
|||||||
updateSessionClient(serverId, client);
|
updateSessionClient(serverId, client);
|
||||||
}, [serverId, client, updateSessionClient]);
|
}, [serverId, client, updateSessionClient]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const serverInfo = client.getLastServerInfoMessage();
|
||||||
|
if (!serverInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSessionServerInfo(serverId, {
|
||||||
|
serverId: serverInfo.serverId,
|
||||||
|
hostname: serverInfo.hostname,
|
||||||
|
version: serverInfo.version,
|
||||||
|
...(serverInfo.capabilities ? { capabilities: serverInfo.capabilities } : {}),
|
||||||
|
...(serverInfo.features ? { features: serverInfo.features } : {}),
|
||||||
|
});
|
||||||
|
}, [client, serverId, updateSessionServerInfo]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverInfo = client.getLastServerInfoMessage();
|
||||||
|
if (!serverInfo?.features?.providersSnapshot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefetchProvidersSnapshot(serverId, client);
|
||||||
|
}, [client, isConnected, serverId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!voiceRuntime) {
|
if (!voiceRuntime) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useQuery, useQueries } from "@tanstack/react-query";
|
|
||||||
import {
|
import {
|
||||||
AGENT_PROVIDER_DEFINITIONS,
|
AGENT_PROVIDER_DEFINITIONS,
|
||||||
type AgentProviderDefinition,
|
type AgentProviderDefinition,
|
||||||
@@ -11,7 +10,6 @@ import type {
|
|||||||
ProviderSnapshotEntry,
|
ProviderSnapshotEntry,
|
||||||
} from "@server/server/agent/agent-sdk-types";
|
} from "@server/server/agent/agent-sdk-types";
|
||||||
import { useHosts } from "@/runtime/host-runtime";
|
import { useHosts } from "@/runtime/host-runtime";
|
||||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
|
||||||
import { useProvidersSnapshot } from "./use-providers-snapshot";
|
import { useProvidersSnapshot } from "./use-providers-snapshot";
|
||||||
import {
|
import {
|
||||||
useFormPreferences,
|
useFormPreferences,
|
||||||
@@ -371,35 +369,19 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
|||||||
}
|
}
|
||||||
}, [isVisible]);
|
}, [isVisible]);
|
||||||
|
|
||||||
// Session state for provider model listing
|
|
||||||
const client = useHostRuntimeClient(formState.serverId ?? "");
|
|
||||||
const isConnected = useHostRuntimeIsConnected(formState.serverId ?? "");
|
|
||||||
const {
|
const {
|
||||||
entries: snapshotEntries,
|
entries: snapshotEntries,
|
||||||
isLoading: snapshotIsLoading,
|
isLoading: snapshotIsLoading,
|
||||||
isFetching: snapshotIsFetching,
|
isFetching: snapshotIsFetching,
|
||||||
supportsSnapshot: supportsProvidersSnapshot,
|
error: snapshotError,
|
||||||
refresh: refreshSnapshot,
|
refresh: refreshSnapshot,
|
||||||
} = useProvidersSnapshot(formState.serverId);
|
} = useProvidersSnapshot(formState.serverId);
|
||||||
|
|
||||||
const [debouncedCwd, setDebouncedCwd] = useState<string | undefined>(undefined);
|
const allProviderEntries = useMemo(() => snapshotEntries ?? [], [snapshotEntries]);
|
||||||
useEffect(() => {
|
|
||||||
const trimmed = formState.workingDir.trim();
|
|
||||||
const next = trimmed.length > 0 ? trimmed : undefined;
|
|
||||||
const timer = setTimeout(() => setDebouncedCwd(next), 180);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [formState.workingDir]);
|
|
||||||
const allProviderEntries = useMemo(
|
|
||||||
() => (supportsProvidersSnapshot ? snapshotEntries ?? [] : undefined),
|
|
||||||
[snapshotEntries, supportsProvidersSnapshot],
|
|
||||||
);
|
|
||||||
const snapshotProviderDefinitions = useMemo(() => {
|
const snapshotProviderDefinitions = useMemo(() => {
|
||||||
if (!supportsProvidersSnapshot) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const snapshotProviders = new Set((snapshotEntries ?? []).map((entry) => entry.provider));
|
const snapshotProviders = new Set((snapshotEntries ?? []).map((entry) => entry.provider));
|
||||||
return allProviderDefinitions.filter((definition) => snapshotProviders.has(definition.id));
|
return allProviderDefinitions.filter((definition) => snapshotProviders.has(definition.id));
|
||||||
}, [snapshotEntries, supportsProvidersSnapshot]);
|
}, [snapshotEntries]);
|
||||||
const snapshotProviderDefinitionMap = useMemo(
|
const snapshotProviderDefinitionMap = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new Map<AgentProvider, AgentProviderDefinition>(
|
new Map<AgentProvider, AgentProviderDefinition>(
|
||||||
@@ -435,157 +417,13 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
|||||||
snapshotSelectedEntry?.modes ??
|
snapshotSelectedEntry?.modes ??
|
||||||
snapshotProviderDefinitionMap.get(formState.provider)?.modes ??
|
snapshotProviderDefinitionMap.get(formState.provider)?.modes ??
|
||||||
[];
|
[];
|
||||||
|
const providerDefinitions = snapshotProviderDefinitions;
|
||||||
// COMPAT(providersSnapshot): legacy fallback for daemons without snapshot support — remove when all daemons support snapshots
|
const providerDefinitionMap = snapshotProviderDefinitionMap;
|
||||||
const availableProvidersQuery = useQuery({
|
const selectableProviderDefinitionMap = snapshotSelectableProviderDefinitionMap;
|
||||||
queryKey: ["availableProviders", formState.serverId],
|
const allProviderModels = snapshotAllProviderModels;
|
||||||
enabled: Boolean(
|
const availableModels = snapshotSelectedProviderModels;
|
||||||
!supportsProvidersSnapshot &&
|
const modeOptions = snapshotSelectedProviderModes;
|
||||||
isVisible &&
|
const isAllModelsLoading = snapshotIsLoading || snapshotIsFetching;
|
||||||
isTargetDaemonReady &&
|
|
||||||
formState.serverId &&
|
|
||||||
client &&
|
|
||||||
isConnected,
|
|
||||||
),
|
|
||||||
staleTime: 60 * 1000,
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!client) {
|
|
||||||
throw new Error("Host is not connected");
|
|
||||||
}
|
|
||||||
const payload = await client.listAvailableProviders();
|
|
||||||
if (payload.error) {
|
|
||||||
throw new Error(payload.error);
|
|
||||||
}
|
|
||||||
return payload.providers.filter((entry) => entry.available).map((entry) => entry.provider);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const legacyProviderDefinitions = useMemo(() => {
|
|
||||||
const availableProviders = availableProvidersQuery.data;
|
|
||||||
if (!availableProviders) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const available = new Set(availableProviders);
|
|
||||||
return allProviderDefinitions.filter((definition) => available.has(definition.id));
|
|
||||||
}, [availableProvidersQuery.data]);
|
|
||||||
const legacyProviderDefinitionMap = useMemo(
|
|
||||||
() =>
|
|
||||||
new Map<AgentProvider, AgentProviderDefinition>(
|
|
||||||
legacyProviderDefinitions.map((definition) => [definition.id, definition]),
|
|
||||||
),
|
|
||||||
[legacyProviderDefinitions],
|
|
||||||
);
|
|
||||||
|
|
||||||
// COMPAT(providersSnapshot): legacy fallback for daemons without snapshot support — remove when all daemons support snapshots
|
|
||||||
const legacySelectedProviderModelsQuery = useQuery({
|
|
||||||
queryKey: ["providerModels", formState.serverId, formState.provider],
|
|
||||||
enabled: Boolean(
|
|
||||||
!supportsProvidersSnapshot &&
|
|
||||||
isVisible &&
|
|
||||||
isTargetDaemonReady &&
|
|
||||||
formState.serverId &&
|
|
||||||
client &&
|
|
||||||
isConnected &&
|
|
||||||
legacyProviderDefinitionMap.has(formState.provider),
|
|
||||||
),
|
|
||||||
staleTime: 5 * 60 * 1000,
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!client) {
|
|
||||||
throw new Error("Host is not connected");
|
|
||||||
}
|
|
||||||
const payload = await client.listProviderModels(formState.provider, {
|
|
||||||
cwd: debouncedCwd,
|
|
||||||
});
|
|
||||||
if (payload.error) {
|
|
||||||
throw new Error(payload.error);
|
|
||||||
}
|
|
||||||
return payload.models ?? [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const legacySelectedProviderModels = legacySelectedProviderModelsQuery.data ?? null;
|
|
||||||
|
|
||||||
// COMPAT(providersSnapshot): legacy fallback for daemons without snapshot support — remove when all daemons support snapshots
|
|
||||||
const providerModesQuery = useQuery({
|
|
||||||
queryKey: ["providerModes", formState.serverId, formState.provider, debouncedCwd],
|
|
||||||
enabled: Boolean(
|
|
||||||
!supportsProvidersSnapshot &&
|
|
||||||
isVisible &&
|
|
||||||
isTargetDaemonReady &&
|
|
||||||
formState.serverId &&
|
|
||||||
client &&
|
|
||||||
isConnected &&
|
|
||||||
legacyProviderDefinitionMap.has(formState.provider),
|
|
||||||
),
|
|
||||||
staleTime: 5 * 60 * 1000,
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!client) {
|
|
||||||
throw new Error("Host is not connected");
|
|
||||||
}
|
|
||||||
const payload = await client.listProviderModes(formState.provider, {
|
|
||||||
cwd: debouncedCwd,
|
|
||||||
});
|
|
||||||
if (payload.error) {
|
|
||||||
throw new Error(payload.error);
|
|
||||||
}
|
|
||||||
return payload.modes ?? [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// COMPAT(providersSnapshot): legacy fallback for daemons without snapshot support — remove when all daemons support snapshots
|
|
||||||
const allProviderModelQueries = useQueries({
|
|
||||||
queries: (supportsProvidersSnapshot ? [] : legacyProviderDefinitions).map((def) => ({
|
|
||||||
queryKey: ["providerModels", formState.serverId, def.id],
|
|
||||||
enabled: Boolean(
|
|
||||||
isVisible && isTargetDaemonReady && formState.serverId && client && isConnected,
|
|
||||||
),
|
|
||||||
staleTime: 5 * 60 * 1000,
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!client) {
|
|
||||||
throw new Error("Host is not connected");
|
|
||||||
}
|
|
||||||
const payload = await client.listProviderModels(def.id as AgentProvider, {
|
|
||||||
cwd: debouncedCwd,
|
|
||||||
});
|
|
||||||
if (payload.error) {
|
|
||||||
throw new Error(payload.error);
|
|
||||||
}
|
|
||||||
return payload.models ?? [];
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
const legacyAllProviderModels = useMemo(() => {
|
|
||||||
const map = new Map<string, AgentModelDefinition[]>();
|
|
||||||
for (let i = 0; i < legacyProviderDefinitions.length; i++) {
|
|
||||||
const query = allProviderModelQueries[i];
|
|
||||||
if (query?.data) {
|
|
||||||
map.set(legacyProviderDefinitions[i]!.id, query.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}, [allProviderModelQueries, legacyProviderDefinitions]);
|
|
||||||
const legacySelectedProviderModes =
|
|
||||||
providerModesQuery.data ?? legacyProviderDefinitionMap.get(formState.provider)?.modes ?? [];
|
|
||||||
|
|
||||||
const providerDefinitions = supportsProvidersSnapshot
|
|
||||||
? snapshotProviderDefinitions
|
|
||||||
: legacyProviderDefinitions;
|
|
||||||
const providerDefinitionMap = supportsProvidersSnapshot
|
|
||||||
? snapshotProviderDefinitionMap
|
|
||||||
: legacyProviderDefinitionMap;
|
|
||||||
const selectableProviderDefinitionMap = supportsProvidersSnapshot
|
|
||||||
? snapshotSelectableProviderDefinitionMap
|
|
||||||
: legacyProviderDefinitionMap;
|
|
||||||
const allProviderModels = supportsProvidersSnapshot
|
|
||||||
? snapshotAllProviderModels
|
|
||||||
: legacyAllProviderModels;
|
|
||||||
const availableModels = supportsProvidersSnapshot
|
|
||||||
? snapshotSelectedProviderModels
|
|
||||||
: legacySelectedProviderModels;
|
|
||||||
const modeOptions = supportsProvidersSnapshot
|
|
||||||
? snapshotSelectedProviderModes
|
|
||||||
: legacySelectedProviderModes;
|
|
||||||
const isAllModelsLoading = supportsProvidersSnapshot
|
|
||||||
? snapshotIsLoading || snapshotIsFetching
|
|
||||||
: allProviderModelQueries.some((q) => q.isLoading);
|
|
||||||
|
|
||||||
// Combine initialValues with initialServerId for resolution
|
// Combine initialValues with initialServerId for resolution
|
||||||
const combinedInitialValues = useMemo((): FormInitialValues | undefined => {
|
const combinedInitialValues = useMemo((): FormInitialValues | undefined => {
|
||||||
@@ -807,12 +645,8 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const refreshProviderModels = useCallback(() => {
|
const refreshProviderModels = useCallback(() => {
|
||||||
if (supportsProvidersSnapshot) {
|
refreshSnapshot();
|
||||||
refreshSnapshot();
|
}, [refreshSnapshot]);
|
||||||
return;
|
|
||||||
}
|
|
||||||
void legacySelectedProviderModelsQuery.refetch();
|
|
||||||
}, [legacySelectedProviderModelsQuery, refreshSnapshot, supportsProvidersSnapshot]);
|
|
||||||
|
|
||||||
const persistFormPreferences = useCallback(async () => {
|
const persistFormPreferences = useCallback(async () => {
|
||||||
const resolvedModel = resolveEffectiveModel(availableModels, formState.model);
|
const resolvedModel = resolveEffectiveModel(availableModels, formState.model);
|
||||||
@@ -848,15 +682,8 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
|||||||
const effectiveModel = resolveEffectiveModel(availableModels, formState.model);
|
const effectiveModel = resolveEffectiveModel(availableModels, formState.model);
|
||||||
const resolvedModelId = effectiveModel?.id ?? formState.model;
|
const resolvedModelId = effectiveModel?.id ?? formState.model;
|
||||||
const availableThinkingOptions = effectiveModel?.thinkingOptions ?? [];
|
const availableThinkingOptions = effectiveModel?.thinkingOptions ?? [];
|
||||||
const isModelLoading = supportsProvidersSnapshot
|
const isModelLoading = snapshotIsLoading || snapshotIsFetching;
|
||||||
? snapshotIsLoading || snapshotIsFetching
|
const modelError = snapshotError;
|
||||||
: legacySelectedProviderModelsQuery.isLoading || legacySelectedProviderModelsQuery.isFetching;
|
|
||||||
const modelError =
|
|
||||||
supportsProvidersSnapshot
|
|
||||||
? null
|
|
||||||
: legacySelectedProviderModelsQuery.error instanceof Error
|
|
||||||
? legacySelectedProviderModelsQuery.error.message
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const workingDirIsEmpty = !formState.workingDir.trim();
|
const workingDirIsEmpty = !formState.workingDir.trim();
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface UseProvidersSnapshotResult {
|
|||||||
entries: ProviderSnapshotEntry[] | undefined;
|
entries: ProviderSnapshotEntry[] | undefined;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
|
error: string | null;
|
||||||
supportsSnapshot: boolean;
|
supportsSnapshot: boolean;
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
}
|
}
|
||||||
@@ -69,6 +70,7 @@ export function useProvidersSnapshot(serverId: string | null): UseProvidersSnaps
|
|||||||
entries: snapshotQuery.data?.entries ?? undefined,
|
entries: snapshotQuery.data?.entries ?? undefined,
|
||||||
isLoading: snapshotQuery.isLoading,
|
isLoading: snapshotQuery.isLoading,
|
||||||
isFetching: snapshotQuery.isFetching,
|
isFetching: snapshotQuery.isFetching,
|
||||||
|
error: snapshotQuery.error instanceof Error ? snapshotQuery.error.message : null,
|
||||||
supportsSnapshot,
|
supportsSnapshot,
|
||||||
refresh,
|
refresh,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ import {
|
|||||||
workspaceTabTargetsEqual,
|
workspaceTabTargetsEqual,
|
||||||
} from "@/utils/workspace-tab-identity";
|
} from "@/utils/workspace-tab-identity";
|
||||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||||
import { prefetchProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
|
||||||
import { useWorkspaceTerminalSessionRetention } from "@/terminal/hooks/use-workspace-terminal-session-retention";
|
import { useWorkspaceTerminalSessionRetention } from "@/terminal/hooks/use-workspace-terminal-session-retention";
|
||||||
import {
|
import {
|
||||||
checkoutStatusQueryKey,
|
checkoutStatusQueryKey,
|
||||||
@@ -609,16 +608,6 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
const client = useHostRuntimeClient(normalizedServerId);
|
const client = useHostRuntimeClient(normalizedServerId);
|
||||||
const isConnected = useHostRuntimeIsConnected(normalizedServerId);
|
const isConnected = useHostRuntimeIsConnected(normalizedServerId);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!client || !isConnected || !normalizedServerId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const session = useSessionStore.getState().sessions[normalizedServerId];
|
|
||||||
if (session?.serverInfo?.features?.providersSnapshot) {
|
|
||||||
prefetchProvidersSnapshot(normalizedServerId, client);
|
|
||||||
}
|
|
||||||
}, [client, isConnected, normalizedServerId]);
|
|
||||||
|
|
||||||
const workspaceAgentVisibility = useStoreWithEqualityFn(
|
const workspaceAgentVisibility = useStoreWithEqualityFn(
|
||||||
useSessionStore,
|
useSessionStore,
|
||||||
(state) =>
|
(state) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user