diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx
index b79c6402f..ee7a63397 100644
--- a/packages/app/src/components/agent-status-bar.tsx
+++ b/packages/app/src/components/agent-status-bar.tsx
@@ -17,6 +17,7 @@ import { getProviderIcon } from "@/components/provider-icons";
import { CombinedModelSelector } from "@/components/combined-model-selector";
import { useQuery } from "@tanstack/react-query";
import { useSessionStore } from "@/stores/session-store";
+import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
import {
buildFavoriteModelKey,
mergeProviderPreferences,
@@ -266,7 +267,7 @@ function ControlledStatusBar({
return null;
}
- const modelDisabled = disabled || isModelLoading || !modelOptions || modelOptions.length === 0;
+ const modelDisabled = disabled;
const SEARCH_THRESHOLD = 6;
@@ -673,6 +674,7 @@ function ControlledStatusBar({
pointerEvents="none"
testID="agent-preferences-model"
>
+
{selectedModelLabel}
@@ -698,6 +700,7 @@ function ControlledStatusBar({
accessibilityLabel="Select thinking option"
testID="agent-preferences-thinking"
>
+
{displayThinking}
@@ -869,9 +872,17 @@ export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStat
);
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__"],
- enabled: Boolean(client && agent?.provider),
+ enabled: Boolean(!supportsSnapshot && client && agent?.provider),
staleTime: 5 * 60 * 1000,
queryFn: async () => {
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(() => {
+ 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);
return definition ? [definition] : [];
- }, [agent?.provider]);
-
- 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 ?? [];
- },
- });
+ }, [agent?.provider, supportsSnapshot, snapshotEntries]);
const agentProviderModels = useMemo(() => {
const map = new Map();
- if (agent?.provider && agentProviderModelQuery.data) {
- map.set(agent.provider, agentProviderModelQuery.data);
+ if (supportsSnapshot && snapshotEntries) {
+ 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;
- }, [agent?.provider, agentProviderModelQuery.data]);
-
- const models = modelsQuery.data ?? null;
+ }, [supportsSnapshot, snapshotEntries, agent?.provider, legacyModelsQuery.data]);
const displayMode =
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);
});
}}
- isModelLoading={isProviderModelsQueryLoading(modelsQuery)}
+ isModelLoading={supportsSnapshot ? (snapshotIsLoading || snapshotIsFetching) : isProviderModelsQueryLoading(legacyModelsQuery)}
onDropdownClose={onDropdownClose}
disabled={!client}
/>
diff --git a/packages/app/src/components/combined-model-selector.tsx b/packages/app/src/components/combined-model-selector.tsx
index a8aaa051d..ffb0ef6df 100644
--- a/packages/app/src/components/combined-model-selector.tsx
+++ b/packages/app/src/components/combined-model-selector.tsx
@@ -25,7 +25,6 @@ import type { AgentProviderDefinition } from "@server/server/agent/provider-mani
const IS_WEB = Platform.OS === "web";
import { Combobox, ComboboxItem } from "@/components/ui/combobox";
-import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { getProviderIcon } from "@/components/provider-icons";
import type { FavoriteModelRow } from "@/hooks/use-form-preferences";
import {
@@ -36,6 +35,9 @@ import {
type SelectorModelRow,
} 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 =
| { kind: "all" }
| { kind: "provider"; providerId: string; providerLabel: string };
@@ -161,7 +163,6 @@ function ModelRow({
}) {
const { theme } = useUnistyles();
const ProviderIcon = getProviderIcon(row.provider);
- const isWeb = Platform.OS === "web";
const handleToggleFavorite = useCallback(
(event: GestureResponderEvent) => {
@@ -171,9 +172,13 @@ function ModelRow({
[onToggleFavorite, row.modelId, row.provider],
);
- const item = (
+ const showDescription =
+ row.description && PROVIDERS_WITH_MODEL_DESCRIPTIONS.has(row.provider);
+
+ return (
);
-
- if (!isWeb || !row.description) {
- return item;
- }
-
- return (
-
-
- {item}
-
-
- {row.description}
-
-
- );
}
function FavoritesSection({
@@ -839,10 +829,6 @@ const styles = StyleSheet.create((theme) => ({
favoriteButtonPressed: {
backgroundColor: theme.colors.surface1,
},
- tooltipText: {
- color: theme.colors.foreground,
- fontSize: theme.fontSize.xs,
- },
sheetLoadingState: {
minHeight: 160,
justifyContent: "center",
diff --git a/packages/app/src/components/ui/combobox.tsx b/packages/app/src/components/ui/combobox.tsx
index ab4ec98f8..9d709cbd7 100644
--- a/packages/app/src/components/ui/combobox.tsx
+++ b/packages/app/src/components/ui/combobox.tsx
@@ -203,12 +203,12 @@ export function ComboboxItem({
]}
>
{leadingContent}
-
+
{label}
{description ? (
-
+
{description}
) : null}
@@ -847,6 +847,11 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
flexShrink: 1,
},
+ comboboxItemContentInline: {
+ flexDirection: "row",
+ alignItems: "baseline",
+ gap: theme.spacing[2],
+ },
comboboxItemLeadingSlot: {
width: 16,
alignItems: "center",
@@ -857,9 +862,9 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foreground,
},
comboboxItemDescription: {
- marginTop: 2,
fontSize: theme.fontSize.xs,
color: theme.colors.foregroundMuted,
+ flexShrink: 1,
},
emptyText: {
paddingHorizontal: theme.spacing[3],
diff --git a/packages/app/src/hooks/use-agent-form-state.ts b/packages/app/src/hooks/use-agent-form-state.ts
index 812e3fd0a..5d5865714 100644
--- a/packages/app/src/hooks/use-agent-form-state.ts
+++ b/packages/app/src/hooks/use-agent-form-state.ts
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { useQuery, useQueries, useQueryClient } from "@tanstack/react-query";
+import { useQuery, useQueries } from "@tanstack/react-query";
import {
AGENT_PROVIDER_DEFINITIONS,
type AgentProviderDefinition,
@@ -12,7 +12,7 @@ import type {
} from "@server/server/agent/agent-sdk-types";
import { useHosts } from "@/runtime/host-runtime";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
-import { useSessionForServer } from "./use-session-directory";
+import { useProvidersSnapshot } from "./use-providers-snapshot";
import {
useFormPreferences,
mergeProviderPreferences,
@@ -322,10 +322,6 @@ function combineInitialValues(
return initialValues;
}
-function providersSnapshotQueryKey(serverId: string | null, cwd: string | undefined) {
- return ["providersSnapshot", serverId, cwd ?? ""] as const;
-}
-
export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAgentFormStateResult {
const {
initialServerId = null,
@@ -343,7 +339,6 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
} = useFormPreferences();
const daemons = useHosts();
- const queryClient = useQueryClient();
// Build a set of valid server IDs for preference validation
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
const client = useHostRuntimeClient(formState.serverId ?? "");
const isConnected = useHostRuntimeIsConnected(formState.serverId ?? "");
- const supportsProvidersSnapshot = useSessionForServer(
- formState.serverId,
- (session) => session?.serverInfo?.features?.providersSnapshot === true,
- );
+ const {
+ entries: snapshotEntries,
+ isLoading: snapshotIsLoading,
+ isFetching: snapshotIsFetching,
+ supportsSnapshot: supportsProvidersSnapshot,
+ refresh: refreshSnapshot,
+ } = useProvidersSnapshot(formState.serverId);
const [debouncedCwd, setDebouncedCwd] = useState(undefined);
useEffect(() => {
@@ -391,69 +389,6 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
const timer = setTimeout(() => setDebouncedCwd(next), 180);
return () => clearTimeout(timer);
}, [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(
() => (supportsProvidersSnapshot ? snapshotEntries ?? [] : undefined),
[snapshotEntries, supportsProvidersSnapshot],
@@ -649,7 +584,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
? snapshotSelectedProviderModes
: legacySelectedProviderModes;
const isAllModelsLoading = supportsProvidersSnapshot
- ? providersSnapshotQuery.isLoading || providersSnapshotQuery.isFetching
+ ? snapshotIsLoading || snapshotIsFetching
: allProviderModelQueries.some((q) => q.isLoading);
// Combine initialValues with initialServerId for resolution
@@ -873,14 +808,11 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
const refreshProviderModels = useCallback(() => {
if (supportsProvidersSnapshot) {
- if (!client) {
- return;
- }
- void client.refreshProvidersSnapshot({ cwd: debouncedCwd });
+ refreshSnapshot();
return;
}
void legacySelectedProviderModelsQuery.refetch();
- }, [client, debouncedCwd, legacySelectedProviderModelsQuery, supportsProvidersSnapshot]);
+ }, [legacySelectedProviderModelsQuery, refreshSnapshot, supportsProvidersSnapshot]);
const persistFormPreferences = useCallback(async () => {
const resolvedModel = resolveEffectiveModel(availableModels, formState.model);
@@ -917,13 +849,11 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
const resolvedModelId = effectiveModel?.id ?? formState.model;
const availableThinkingOptions = effectiveModel?.thinkingOptions ?? [];
const isModelLoading = supportsProvidersSnapshot
- ? providersSnapshotQuery.isLoading || providersSnapshotQuery.isFetching
+ ? snapshotIsLoading || snapshotIsFetching
: legacySelectedProviderModelsQuery.isLoading || legacySelectedProviderModelsQuery.isFetching;
const modelError =
supportsProvidersSnapshot
- ? providersSnapshotQuery.error instanceof Error
- ? providersSnapshotQuery.error.message
- : null
+ ? null
: legacySelectedProviderModelsQuery.error instanceof Error
? legacySelectedProviderModelsQuery.error.message
: null;
diff --git a/packages/app/src/hooks/use-providers-snapshot.ts b/packages/app/src/hooks/use-providers-snapshot.ts
new file mode 100644
index 000000000..943791f8a
--- /dev/null
+++ b/packages/app/src/hooks/use-providers-snapshot.ts
@@ -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(),
+ });
+}
diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx
index d4555c4ab..dbeb7f930 100644
--- a/packages/app/src/screens/workspace/workspace-screen.tsx
+++ b/packages/app/src/screens/workspace/workspace-screen.tsx
@@ -70,6 +70,7 @@ import {
workspaceTabTargetsEqual,
} from "@/utils/workspace-tab-identity";
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 {
checkoutStatusQueryKey,
@@ -608,6 +609,16 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
const client = useHostRuntimeClient(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(
useSessionStore,
(state) =>