feat(app): unified provider snapshot, resilient model selector, and UX polish
- Model selector is no longer disabled while providers load; opens immediately and streams available providers as they arrive - Selecting a non-default provider on mobile now works correctly - Provider icon added to model trigger in mobile preferences sheet - Thinking icon (brain) added to thinking trigger in mobile preferences - Model descriptions for OpenCode/Pi shown inline next to model name instead of in a tooltip - Running agents and draft screen share a single provider/model cache, eliminating duplicate network requests - Provider data is prefetched when entering a workspace so the model selector is instant on first open
This commit is contained in:
@@ -17,6 +17,7 @@ import { getProviderIcon } from "@/components/provider-icons";
|
|||||||
import { CombinedModelSelector } from "@/components/combined-model-selector";
|
import { CombinedModelSelector } from "@/components/combined-model-selector";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
|
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
||||||
import {
|
import {
|
||||||
buildFavoriteModelKey,
|
buildFavoriteModelKey,
|
||||||
mergeProviderPreferences,
|
mergeProviderPreferences,
|
||||||
@@ -266,7 +267,7 @@ function ControlledStatusBar({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelDisabled = disabled || isModelLoading || !modelOptions || modelOptions.length === 0;
|
const modelDisabled = disabled;
|
||||||
|
|
||||||
const SEARCH_THRESHOLD = 6;
|
const SEARCH_THRESHOLD = 6;
|
||||||
|
|
||||||
@@ -673,6 +674,7 @@ function ControlledStatusBar({
|
|||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
testID="agent-preferences-model"
|
testID="agent-preferences-model"
|
||||||
>
|
>
|
||||||
|
<ProviderIcon size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||||
<Text style={styles.sheetSelectText}>{selectedModelLabel}</Text>
|
<Text style={styles.sheetSelectText}>{selectedModelLabel}</Text>
|
||||||
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||||
</View>
|
</View>
|
||||||
@@ -698,6 +700,7 @@ function ControlledStatusBar({
|
|||||||
accessibilityLabel="Select thinking option"
|
accessibilityLabel="Select thinking option"
|
||||||
testID="agent-preferences-thinking"
|
testID="agent-preferences-thinking"
|
||||||
>
|
>
|
||||||
|
<Brain size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||||
<Text style={styles.sheetSelectText}>{displayThinking}</Text>
|
<Text style={styles.sheetSelectText}>{displayThinking}</Text>
|
||||||
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -869,9 +872,17 @@ export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStat
|
|||||||
);
|
);
|
||||||
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
||||||
|
|
||||||
const modelsQuery = useQuery({
|
const {
|
||||||
|
entries: snapshotEntries,
|
||||||
|
isLoading: snapshotIsLoading,
|
||||||
|
isFetching: snapshotIsFetching,
|
||||||
|
supportsSnapshot,
|
||||||
|
} = useProvidersSnapshot(serverId);
|
||||||
|
|
||||||
|
// COMPAT(providersSnapshot): legacy fallback for daemons without snapshot support
|
||||||
|
const legacyModelsQuery = useQuery({
|
||||||
queryKey: ["providerModels", serverId, agent?.provider ?? "__missing_provider__"],
|
queryKey: ["providerModels", serverId, agent?.provider ?? "__missing_provider__"],
|
||||||
enabled: Boolean(client && agent?.provider),
|
enabled: Boolean(!supportsSnapshot && client && agent?.provider),
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!client || !agent) {
|
if (!client || !agent) {
|
||||||
@@ -885,36 +896,39 @@ export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStat
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const snapshotModels = useMemo(() => {
|
||||||
|
if (!supportsSnapshot || !snapshotEntries || !agent?.provider) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const entry = snapshotEntries.find((e) => e.provider === agent.provider);
|
||||||
|
return entry?.models ?? null;
|
||||||
|
}, [supportsSnapshot, snapshotEntries, agent?.provider]);
|
||||||
|
|
||||||
|
const models = supportsSnapshot ? snapshotModels : (legacyModelsQuery.data ?? null);
|
||||||
|
|
||||||
const agentProviderDefinitions = useMemo(() => {
|
const agentProviderDefinitions = useMemo(() => {
|
||||||
|
if (supportsSnapshot && 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]);
|
}, [agent?.provider, supportsSnapshot, snapshotEntries]);
|
||||||
|
|
||||||
const agentProviderModelQuery = useQuery({
|
|
||||||
queryKey: ["providerModels", serverId, agent?.provider, agent?.cwd ?? ""],
|
|
||||||
enabled: Boolean(client && agent?.cwd && agent?.provider),
|
|
||||||
staleTime: 5 * 60 * 1000,
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!client || !agent) {
|
|
||||||
throw new Error("Daemon client unavailable");
|
|
||||||
}
|
|
||||||
const payload = await client.listProviderModels(agent.provider, { cwd: agent.cwd });
|
|
||||||
if (payload.error) {
|
|
||||||
throw new Error(payload.error);
|
|
||||||
}
|
|
||||||
return payload.models ?? [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const agentProviderModels = useMemo(() => {
|
const agentProviderModels = useMemo(() => {
|
||||||
const map = new Map<string, AgentModelDefinition[]>();
|
const map = new Map<string, AgentModelDefinition[]>();
|
||||||
if (agent?.provider && agentProviderModelQuery.data) {
|
if (supportsSnapshot && snapshotEntries) {
|
||||||
map.set(agent.provider, agentProviderModelQuery.data);
|
for (const entry of snapshotEntries) {
|
||||||
|
if (entry.models) {
|
||||||
|
map.set(entry.provider, entry.models);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (agent?.provider && legacyModelsQuery.data) {
|
||||||
|
map.set(agent.provider, legacyModelsQuery.data);
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, [agent?.provider, agentProviderModelQuery.data]);
|
}, [supportsSnapshot, snapshotEntries, agent?.provider, legacyModelsQuery.data]);
|
||||||
|
|
||||||
const models = modelsQuery.data ?? null;
|
|
||||||
|
|
||||||
const displayMode =
|
const displayMode =
|
||||||
availableModes.find((mode) => mode.id === agent?.currentModeId)?.label ||
|
availableModes.find((mode) => mode.id === agent?.currentModeId)?.label ||
|
||||||
@@ -1034,7 +1048,7 @@ export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStat
|
|||||||
console.warn("[AgentStatusBar] setAgentFeature failed", error);
|
console.warn("[AgentStatusBar] setAgentFeature failed", error);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
isModelLoading={isProviderModelsQueryLoading(modelsQuery)}
|
isModelLoading={supportsSnapshot ? (snapshotIsLoading || snapshotIsFetching) : isProviderModelsQueryLoading(legacyModelsQuery)}
|
||||||
onDropdownClose={onDropdownClose}
|
onDropdownClose={onDropdownClose}
|
||||||
disabled={!client}
|
disabled={!client}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import type { AgentProviderDefinition } from "@server/server/agent/provider-mani
|
|||||||
const IS_WEB = Platform.OS === "web";
|
const IS_WEB = Platform.OS === "web";
|
||||||
|
|
||||||
import { Combobox, ComboboxItem } from "@/components/ui/combobox";
|
import { Combobox, ComboboxItem } from "@/components/ui/combobox";
|
||||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
|
||||||
import { getProviderIcon } from "@/components/provider-icons";
|
import { getProviderIcon } from "@/components/provider-icons";
|
||||||
import type { FavoriteModelRow } from "@/hooks/use-form-preferences";
|
import type { FavoriteModelRow } from "@/hooks/use-form-preferences";
|
||||||
import {
|
import {
|
||||||
@@ -36,6 +35,9 @@ import {
|
|||||||
type SelectorModelRow,
|
type SelectorModelRow,
|
||||||
} from "./combined-model-selector.utils";
|
} from "./combined-model-selector.utils";
|
||||||
|
|
||||||
|
// TODO: this should be configured per provider in the provider manifest
|
||||||
|
const PROVIDERS_WITH_MODEL_DESCRIPTIONS = new Set(["opencode", "pi"]);
|
||||||
|
|
||||||
type SelectorView =
|
type SelectorView =
|
||||||
| { kind: "all" }
|
| { kind: "all" }
|
||||||
| { kind: "provider"; providerId: string; providerLabel: string };
|
| { kind: "provider"; providerId: string; providerLabel: string };
|
||||||
@@ -161,7 +163,6 @@ function ModelRow({
|
|||||||
}) {
|
}) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const ProviderIcon = getProviderIcon(row.provider);
|
const ProviderIcon = getProviderIcon(row.provider);
|
||||||
const isWeb = Platform.OS === "web";
|
|
||||||
|
|
||||||
const handleToggleFavorite = useCallback(
|
const handleToggleFavorite = useCallback(
|
||||||
(event: GestureResponderEvent) => {
|
(event: GestureResponderEvent) => {
|
||||||
@@ -171,9 +172,13 @@ function ModelRow({
|
|||||||
[onToggleFavorite, row.modelId, row.provider],
|
[onToggleFavorite, row.modelId, row.provider],
|
||||||
);
|
);
|
||||||
|
|
||||||
const item = (
|
const showDescription =
|
||||||
|
row.description && PROVIDERS_WITH_MODEL_DESCRIPTIONS.has(row.provider);
|
||||||
|
|
||||||
|
return (
|
||||||
<ComboboxItem
|
<ComboboxItem
|
||||||
label={row.modelLabel}
|
label={row.modelLabel}
|
||||||
|
description={showDescription ? row.description : undefined}
|
||||||
selected={isSelected}
|
selected={isSelected}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
elevated={elevated}
|
elevated={elevated}
|
||||||
@@ -211,21 +216,6 @@ function ModelRow({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isWeb || !row.description) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
|
||||||
<TooltipTrigger asChild triggerRefProp="ref">
|
|
||||||
<View>{item}</View>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right" align="center" offset={4}>
|
|
||||||
<Text style={styles.tooltipText}>{row.description}</Text>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function FavoritesSection({
|
function FavoritesSection({
|
||||||
@@ -839,10 +829,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
favoriteButtonPressed: {
|
favoriteButtonPressed: {
|
||||||
backgroundColor: theme.colors.surface1,
|
backgroundColor: theme.colors.surface1,
|
||||||
},
|
},
|
||||||
tooltipText: {
|
|
||||||
color: theme.colors.foreground,
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
},
|
|
||||||
sheetLoadingState: {
|
sheetLoadingState: {
|
||||||
minHeight: 160,
|
minHeight: 160,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
|||||||
@@ -203,12 +203,12 @@ export function ComboboxItem({
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{leadingContent}
|
{leadingContent}
|
||||||
<View style={styles.comboboxItemContent}>
|
<View style={[styles.comboboxItemContent, description && styles.comboboxItemContentInline]}>
|
||||||
<Text numberOfLines={1} style={styles.comboboxItemLabel}>
|
<Text numberOfLines={1} style={styles.comboboxItemLabel}>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
{description ? (
|
{description ? (
|
||||||
<Text numberOfLines={2} style={styles.comboboxItemDescription}>
|
<Text numberOfLines={1} style={styles.comboboxItemDescription}>
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -847,6 +847,11 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
},
|
},
|
||||||
|
comboboxItemContentInline: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "baseline",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
},
|
||||||
comboboxItemLeadingSlot: {
|
comboboxItemLeadingSlot: {
|
||||||
width: 16,
|
width: 16,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -857,9 +862,9 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
},
|
},
|
||||||
comboboxItemDescription: {
|
comboboxItemDescription: {
|
||||||
marginTop: 2,
|
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
|
flexShrink: 1,
|
||||||
},
|
},
|
||||||
emptyText: {
|
emptyText: {
|
||||||
paddingHorizontal: theme.spacing[3],
|
paddingHorizontal: theme.spacing[3],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useQuery, useQueries, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueries } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
AGENT_PROVIDER_DEFINITIONS,
|
AGENT_PROVIDER_DEFINITIONS,
|
||||||
type AgentProviderDefinition,
|
type AgentProviderDefinition,
|
||||||
@@ -12,7 +12,7 @@ import type {
|
|||||||
} 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 { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||||
import { useSessionForServer } from "./use-session-directory";
|
import { useProvidersSnapshot } from "./use-providers-snapshot";
|
||||||
import {
|
import {
|
||||||
useFormPreferences,
|
useFormPreferences,
|
||||||
mergeProviderPreferences,
|
mergeProviderPreferences,
|
||||||
@@ -322,10 +322,6 @@ function combineInitialValues(
|
|||||||
return initialValues;
|
return initialValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
function providersSnapshotQueryKey(serverId: string | null, cwd: string | undefined) {
|
|
||||||
return ["providersSnapshot", serverId, cwd ?? ""] as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAgentFormStateResult {
|
export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAgentFormStateResult {
|
||||||
const {
|
const {
|
||||||
initialServerId = null,
|
initialServerId = null,
|
||||||
@@ -343,7 +339,6 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
|||||||
} = useFormPreferences();
|
} = useFormPreferences();
|
||||||
|
|
||||||
const daemons = useHosts();
|
const daemons = useHosts();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
// Build a set of valid server IDs for preference validation
|
// Build a set of valid server IDs for preference validation
|
||||||
const validServerIds = useMemo(() => new Set(daemons.map((d) => d.serverId)), [daemons]);
|
const validServerIds = useMemo(() => new Set(daemons.map((d) => d.serverId)), [daemons]);
|
||||||
@@ -379,10 +374,13 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
|||||||
// Session state for provider model listing
|
// Session state for provider model listing
|
||||||
const client = useHostRuntimeClient(formState.serverId ?? "");
|
const client = useHostRuntimeClient(formState.serverId ?? "");
|
||||||
const isConnected = useHostRuntimeIsConnected(formState.serverId ?? "");
|
const isConnected = useHostRuntimeIsConnected(formState.serverId ?? "");
|
||||||
const supportsProvidersSnapshot = useSessionForServer(
|
const {
|
||||||
formState.serverId,
|
entries: snapshotEntries,
|
||||||
(session) => session?.serverInfo?.features?.providersSnapshot === true,
|
isLoading: snapshotIsLoading,
|
||||||
);
|
isFetching: snapshotIsFetching,
|
||||||
|
supportsSnapshot: supportsProvidersSnapshot,
|
||||||
|
refresh: refreshSnapshot,
|
||||||
|
} = useProvidersSnapshot(formState.serverId);
|
||||||
|
|
||||||
const [debouncedCwd, setDebouncedCwd] = useState<string | undefined>(undefined);
|
const [debouncedCwd, setDebouncedCwd] = useState<string | undefined>(undefined);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -391,69 +389,6 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
|||||||
const timer = setTimeout(() => setDebouncedCwd(next), 180);
|
const timer = setTimeout(() => setDebouncedCwd(next), 180);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [formState.workingDir]);
|
}, [formState.workingDir]);
|
||||||
|
|
||||||
const snapshotQueryKey = useMemo(
|
|
||||||
() => providersSnapshotQueryKey(formState.serverId, debouncedCwd),
|
|
||||||
[debouncedCwd, formState.serverId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const providersSnapshotQuery = useQuery({
|
|
||||||
queryKey: snapshotQueryKey,
|
|
||||||
enabled: Boolean(
|
|
||||||
supportsProvidersSnapshot &&
|
|
||||||
isVisible &&
|
|
||||||
isTargetDaemonReady &&
|
|
||||||
formState.serverId &&
|
|
||||||
client &&
|
|
||||||
isConnected,
|
|
||||||
),
|
|
||||||
staleTime: 60 * 1000,
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!client) {
|
|
||||||
throw new Error("Host is not connected");
|
|
||||||
}
|
|
||||||
return client.getProvidersSnapshot({ cwd: debouncedCwd });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
!supportsProvidersSnapshot ||
|
|
||||||
!client ||
|
|
||||||
!isConnected ||
|
|
||||||
!isVisible ||
|
|
||||||
!isTargetDaemonReady ||
|
|
||||||
!formState.serverId
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return client.on("providers_snapshot_update", (message) => {
|
|
||||||
if (message.type !== "providers_snapshot_update") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (message.payload.cwd !== undefined && message.payload.cwd !== debouncedCwd) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
queryClient.setQueryData(snapshotQueryKey, {
|
|
||||||
entries: message.payload.entries,
|
|
||||||
generatedAt: message.payload.generatedAt,
|
|
||||||
requestId: "providers_snapshot_update",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
client,
|
|
||||||
debouncedCwd,
|
|
||||||
formState.serverId,
|
|
||||||
isConnected,
|
|
||||||
isTargetDaemonReady,
|
|
||||||
isVisible,
|
|
||||||
queryClient,
|
|
||||||
snapshotQueryKey,
|
|
||||||
supportsProvidersSnapshot,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const snapshotEntries = providersSnapshotQuery.data?.entries ?? undefined;
|
|
||||||
const allProviderEntries = useMemo(
|
const allProviderEntries = useMemo(
|
||||||
() => (supportsProvidersSnapshot ? snapshotEntries ?? [] : undefined),
|
() => (supportsProvidersSnapshot ? snapshotEntries ?? [] : undefined),
|
||||||
[snapshotEntries, supportsProvidersSnapshot],
|
[snapshotEntries, supportsProvidersSnapshot],
|
||||||
@@ -649,7 +584,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
|||||||
? snapshotSelectedProviderModes
|
? snapshotSelectedProviderModes
|
||||||
: legacySelectedProviderModes;
|
: legacySelectedProviderModes;
|
||||||
const isAllModelsLoading = supportsProvidersSnapshot
|
const isAllModelsLoading = supportsProvidersSnapshot
|
||||||
? providersSnapshotQuery.isLoading || providersSnapshotQuery.isFetching
|
? snapshotIsLoading || snapshotIsFetching
|
||||||
: allProviderModelQueries.some((q) => q.isLoading);
|
: allProviderModelQueries.some((q) => q.isLoading);
|
||||||
|
|
||||||
// Combine initialValues with initialServerId for resolution
|
// Combine initialValues with initialServerId for resolution
|
||||||
@@ -873,14 +808,11 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
|||||||
|
|
||||||
const refreshProviderModels = useCallback(() => {
|
const refreshProviderModels = useCallback(() => {
|
||||||
if (supportsProvidersSnapshot) {
|
if (supportsProvidersSnapshot) {
|
||||||
if (!client) {
|
refreshSnapshot();
|
||||||
return;
|
|
||||||
}
|
|
||||||
void client.refreshProvidersSnapshot({ cwd: debouncedCwd });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void legacySelectedProviderModelsQuery.refetch();
|
void legacySelectedProviderModelsQuery.refetch();
|
||||||
}, [client, debouncedCwd, legacySelectedProviderModelsQuery, supportsProvidersSnapshot]);
|
}, [legacySelectedProviderModelsQuery, refreshSnapshot, supportsProvidersSnapshot]);
|
||||||
|
|
||||||
const persistFormPreferences = useCallback(async () => {
|
const persistFormPreferences = useCallback(async () => {
|
||||||
const resolvedModel = resolveEffectiveModel(availableModels, formState.model);
|
const resolvedModel = resolveEffectiveModel(availableModels, formState.model);
|
||||||
@@ -917,13 +849,11 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
|||||||
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 = supportsProvidersSnapshot
|
||||||
? providersSnapshotQuery.isLoading || providersSnapshotQuery.isFetching
|
? snapshotIsLoading || snapshotIsFetching
|
||||||
: legacySelectedProviderModelsQuery.isLoading || legacySelectedProviderModelsQuery.isFetching;
|
: legacySelectedProviderModelsQuery.isLoading || legacySelectedProviderModelsQuery.isFetching;
|
||||||
const modelError =
|
const modelError =
|
||||||
supportsProvidersSnapshot
|
supportsProvidersSnapshot
|
||||||
? providersSnapshotQuery.error instanceof Error
|
? null
|
||||||
? providersSnapshotQuery.error.message
|
|
||||||
: null
|
|
||||||
: legacySelectedProviderModelsQuery.error instanceof Error
|
: legacySelectedProviderModelsQuery.error instanceof Error
|
||||||
? legacySelectedProviderModelsQuery.error.message
|
? legacySelectedProviderModelsQuery.error.message
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import type { ProviderSnapshotEntry } from "@server/server/agent/agent-sdk-types";
|
||||||
|
import type { DaemonClient } from "@server/client/daemon-client";
|
||||||
|
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||||
|
import { useSessionForServer } from "./use-session-directory";
|
||||||
|
import { queryClient as singletonQueryClient } from "@/query/query-client";
|
||||||
|
|
||||||
|
export function providersSnapshotQueryKey(serverId: string | null) {
|
||||||
|
return ["providersSnapshot", serverId] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseProvidersSnapshotResult {
|
||||||
|
entries: ProviderSnapshotEntry[] | undefined;
|
||||||
|
isLoading: boolean;
|
||||||
|
isFetching: boolean;
|
||||||
|
supportsSnapshot: boolean;
|
||||||
|
refresh: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProvidersSnapshot(serverId: string | null): UseProvidersSnapshotResult {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const client = useHostRuntimeClient(serverId ?? "");
|
||||||
|
const isConnected = useHostRuntimeIsConnected(serverId ?? "");
|
||||||
|
const supportsSnapshot = useSessionForServer(
|
||||||
|
serverId,
|
||||||
|
(session) => session?.serverInfo?.features?.providersSnapshot === true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryKey = useMemo(() => providersSnapshotQueryKey(serverId), [serverId]);
|
||||||
|
|
||||||
|
const snapshotQuery = useQuery({
|
||||||
|
queryKey,
|
||||||
|
enabled: Boolean(supportsSnapshot && serverId && client && isConnected),
|
||||||
|
staleTime: 60_000,
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!client) {
|
||||||
|
throw new Error("Host is not connected");
|
||||||
|
}
|
||||||
|
return client.getProvidersSnapshot();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!supportsSnapshot || !client || !isConnected || !serverId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.on("providers_snapshot_update", (message) => {
|
||||||
|
if (message.type !== "providers_snapshot_update") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queryClient.setQueryData(queryKey, {
|
||||||
|
entries: message.payload.entries,
|
||||||
|
generatedAt: message.payload.generatedAt,
|
||||||
|
requestId: "providers_snapshot_update",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [client, isConnected, serverId, queryClient, queryKey, supportsSnapshot]);
|
||||||
|
|
||||||
|
const refresh = useCallback(() => {
|
||||||
|
if (!client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void client.refreshProvidersSnapshot();
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
entries: snapshotQuery.data?.entries ?? undefined,
|
||||||
|
isLoading: snapshotQuery.isLoading,
|
||||||
|
isFetching: snapshotQuery.isFetching,
|
||||||
|
supportsSnapshot,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prefetchProvidersSnapshot(serverId: string, client: DaemonClient): void {
|
||||||
|
const queryKey = providersSnapshotQueryKey(serverId);
|
||||||
|
void singletonQueryClient.prefetchQuery({
|
||||||
|
queryKey,
|
||||||
|
staleTime: 60_000,
|
||||||
|
queryFn: () => client.getProvidersSnapshot(),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -70,6 +70,7 @@ 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,
|
||||||
@@ -608,6 +609,16 @@ 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