diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx
index 015f19c9b..afbd7d866 100644
--- a/packages/app/src/components/agent-status-bar.tsx
+++ b/packages/app/src/components/agent-status-bar.tsx
@@ -6,7 +6,7 @@ import { useStoreWithEqualityFn } from "zustand/traditional";
import { Brain, ChevronDown, ShieldAlert, ShieldCheck, ShieldOff } from "lucide-react-native";
import { getProviderIcon } from "@/components/provider-icons";
import { CombinedModelSelector } from "@/components/combined-model-selector";
-import { useQueries, useQuery } from "@tanstack/react-query";
+import { useQuery } from "@tanstack/react-query";
import { useSessionStore } from "@/stores/session-store";
import {
buildFavoriteModelKey,
@@ -481,41 +481,6 @@ function ControlledStatusBar({
stackBehavior="replace"
testID="agent-preferences-sheet"
>
- {providerOptions && providerOptions.length > 0 ? (
-
-
- [
- styles.sheetSelect,
- pressed && styles.sheetSelectPressed,
- (disabled || !canSelectProvider) && styles.disabledSheetSelect,
- ]}
- accessibilityRole="button"
- accessibilityLabel="Select agent provider"
- testID="agent-preferences-provider"
- >
- {displayProvider}
-
-
-
- {providerOptions.map((provider) => (
- onSelectProvider?.(provider.id)}
- >
- {provider.label}
-
- ))}
-
-
-
- ) : null}
-
{canSelectModel ? (
{
- if (selectedProviderId === provider) {
- onSelectModel?.(modelId);
+ if (selectedProviderId !== provider) {
+ onSelectProvider?.(selectedProviderId);
}
+ onSelectModel?.(modelId);
}}
favoriteKeys={favoriteKeys}
onToggleFavorite={onToggleFavoriteModel}
@@ -676,59 +642,34 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
},
});
- const availableProvidersQuery = useQuery({
- queryKey: ["availableProviders", serverId],
- enabled: Boolean(client),
- staleTime: 60 * 1000,
+ const agentProviderDefinitions = useMemo(() => {
+ 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) {
+ if (!client || !agent) {
throw new Error("Daemon client unavailable");
}
- const payload = await client.listAvailableProviders();
+ const payload = await client.listProviderModels(agent.provider, { cwd: agent.cwd });
if (payload.error) {
throw new Error(payload.error);
}
- return payload.providers.filter((entry) => entry.available).map((entry) => entry.provider);
+ return payload.models ?? [];
},
});
- const availableProviderDefinitions = useMemo(() => {
- const availableProviders = availableProvidersQuery.data;
- if (!availableProviders) {
- return [];
- }
- const available = new Set(availableProviders);
- return AGENT_PROVIDER_DEFINITIONS.filter((definition) => available.has(definition.id));
- }, [availableProvidersQuery.data]);
-
- const allProviderModelQueries = useQueries({
- queries: availableProviderDefinitions.map((definition) => ({
- queryKey: ["providerModels", serverId, definition.id, agent?.cwd ?? ""],
- enabled: Boolean(client && agent?.cwd),
- staleTime: 5 * 60 * 1000,
- queryFn: async () => {
- if (!client || !agent) {
- throw new Error("Daemon client unavailable");
- }
- const payload = await client.listProviderModels(definition.id, { cwd: agent.cwd });
- if (payload.error) {
- throw new Error(payload.error);
- }
- return payload.models ?? [];
- },
- })),
- });
-
- const liveAllProviderModels = useMemo(() => {
+ const agentProviderModels = useMemo(() => {
const map = new Map();
- for (let i = 0; i < availableProviderDefinitions.length; i++) {
- const query = allProviderModelQueries[i];
- if (query?.data) {
- map.set(availableProviderDefinitions[i]!.id, query.data);
- }
+ if (agent?.provider && agentProviderModelQuery.data) {
+ map.set(agent.provider, agentProviderModelQuery.data);
}
return map;
- }, [allProviderModelQueries, availableProviderDefinitions]);
+ }, [agent?.provider, agentProviderModelQuery.data]);
const models = modelsQuery.data ?? null;
@@ -777,9 +718,8 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
modeOptions.length > 0 ? modeOptions : [{ id: agent.currentModeId ?? "", label: displayMode }]
}
selectedModeId={agent.currentModeId ?? undefined}
- providerDefinitions={availableProviderDefinitions}
- allProviderModels={liveAllProviderModels}
- canSelectModelProvider={(providerId) => providerId === agent.provider}
+ providerDefinitions={agentProviderDefinitions}
+ allProviderModels={agentProviderModels}
onSelectMode={(modeId) => {
if (!client) {
return;
@@ -924,29 +864,23 @@ export function DraftAgentStatusBar({
);
}
- const providerOptions = providerDefinitions.map((definition) => ({
- id: definition.id,
- label: definition.label,
+ const modelOptions: StatusOption[] = models.map((model) => ({
+ id: model.id,
+ label: model.label,
}));
- const modelOptions: StatusOption[] = [];
- for (const model of models) {
- modelOptions.push({ id: model.id, label: model.label });
- }
-
return (
onSelectProvider(providerId as AgentProvider)}
+ providerDefinitions={providerDefinitions}
+ allProviderModels={allProviderModels}
modeOptions={mappedModeOptions}
selectedModeId={effectiveSelectedMode}
onSelectMode={onSelectMode}
modelOptions={modelOptions}
selectedModelId={selectedModel}
- onSelectModel={onSelectModel}
- isModelLoading={isModelLoading}
+ onSelectModel={(modelId) => onSelectModel(modelId)}
+ isModelLoading={isAllModelsLoading}
favoriteKeys={favoriteKeys}
onToggleFavoriteModel={(provider, modelId) => {
void updatePreferences(toggleFavoriteModel({ preferences, provider, modelId })).catch((error) => {
diff --git a/packages/app/src/components/combined-model-selector.tsx b/packages/app/src/components/combined-model-selector.tsx
index 159bd774d..1d62aa555 100644
--- a/packages/app/src/components/combined-model-selector.tsx
+++ b/packages/app/src/components/combined-model-selector.tsx
@@ -1,5 +1,12 @@
-import { useCallback, useMemo, useRef, useState } from "react";
-import { View, Text, Pressable, Platform, type GestureResponderEvent } from "react-native";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import {
+ View,
+ Text,
+ Pressable,
+ Platform,
+ ActivityIndicator,
+ type GestureResponderEvent,
+} from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import {
ArrowLeft,
@@ -11,6 +18,7 @@ import {
import type { AgentModelDefinition, AgentProvider } from "@server/server/agent/agent-sdk-types";
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
import { Combobox, ComboboxItem, SearchInput } 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 {
@@ -130,6 +138,7 @@ function ModelRow({
}) {
const { theme } = useUnistyles();
const ProviderIcon = getProviderIcon(row.provider);
+ const isWeb = Platform.OS === "web";
const handleToggleFavorite = useCallback(
(event: GestureResponderEvent) => {
@@ -139,16 +148,9 @@ function ModelRow({
[onToggleFavorite, row.modelId, row.provider],
);
- return (
+ const item = (
-
+ {({ hovered }) => (
+
+ )}
) : null
}
/>
);
+
+ if (!isWeb || !row.description) {
+ return item;
+ }
+
+ return (
+
+
+ {item}
+
+
+ {row.description}
+
+
+ );
}
function FavoritesSection({
@@ -205,7 +230,6 @@ function FavoritesSection({
return (
-
Favorites
{favoriteRows.map((row) => (
@@ -260,7 +284,6 @@ function GroupedProviderRows({
{isInline ? (
<>
-
{providerDefinition?.label ?? group.providerLabel}
@@ -435,8 +458,10 @@ export function CombinedModelSelector({
disabled = false,
}: CombinedModelSelectorProps) {
const { theme } = useUnistyles();
+ const isWeb = Platform.OS === "web";
const anchorRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
+ const [isContentReady, setIsContentReady] = useState(isWeb);
const [view, setView] = useState({ kind: "all" });
const [searchQuery, setSearchQuery] = useState("");
@@ -484,6 +509,23 @@ export function CombinedModelSelector({
return buildSelectedTriggerLabel(selectedProviderLabel, selectedModelLabel);
}, [selectedModelLabel, selectedProviderLabel]);
+ useEffect(() => {
+ if (isWeb) {
+ return;
+ }
+
+ if (!isOpen) {
+ setIsContentReady(false);
+ return;
+ }
+
+ const frame = requestAnimationFrame(() => {
+ setIsContentReady(true);
+ });
+
+ return () => cancelAnimationFrame(frame);
+ }, [isOpen, isWeb]);
+
return (
<>
{}}
open={isOpen}
onOpenChange={handleOpenChange}
+ stackBehavior="push"
anchorRef={anchorRef}
desktopPlacement="top-start"
title="Select model"
>
- {
- setView({ kind: "provider", providerId, providerLabel });
- }}
- onBack={
- view.kind === "provider"
- ? () => {
- setView({ kind: "all" });
- }
- : undefined
- }
- />
+ {isContentReady ? (
+ {
+ setView({ kind: "provider", providerId, providerLabel });
+ }}
+ onBack={
+ view.kind === "provider"
+ ? () => {
+ setView({ kind: "all" });
+ }
+ : undefined
+ }
+ />
+ ) : (
+
+
+ Loading model selector…
+
+ )}
>
);
@@ -670,4 +720,18 @@ const styles = StyleSheet.create((theme) => ({
favoriteButtonPressed: {
backgroundColor: theme.colors.surface1,
},
+ tooltipText: {
+ color: theme.colors.foreground,
+ fontSize: theme.fontSize.xs,
+ },
+ sheetLoadingState: {
+ minHeight: 160,
+ justifyContent: "center",
+ alignItems: "center",
+ gap: theme.spacing[2],
+ },
+ sheetLoadingText: {
+ color: theme.colors.foregroundMuted,
+ fontSize: theme.fontSize.sm,
+ },
}));
diff --git a/packages/app/src/components/combined-model-selector.utils.ts b/packages/app/src/components/combined-model-selector.utils.ts
index fd1f07144..fa7f102d3 100644
--- a/packages/app/src/components/combined-model-selector.utils.ts
+++ b/packages/app/src/components/combined-model-selector.utils.ts
@@ -12,7 +12,7 @@ export function resolveProviderLabel(
}
export function buildSelectedTriggerLabel(providerLabel: string, modelLabel: string): string {
- return `${providerLabel}: ${modelLabel}`;
+ return modelLabel;
}
export function buildModelRows(
diff --git a/packages/app/src/components/ui/combobox.tsx b/packages/app/src/components/ui/combobox.tsx
index cffec42fd..179f94c15 100644
--- a/packages/app/src/components/ui/combobox.tsx
+++ b/packages/app/src/components/ui/combobox.tsx
@@ -65,6 +65,7 @@ export interface ComboboxProps {
open?: boolean;
onOpenChange?: (open: boolean) => void;
enableDismissOnClose?: boolean;
+ stackBehavior?: "push" | "switch" | "replace";
desktopPlacement?: "top-start" | "bottom-start";
/**
* Prevents an initial frame at 0,0 by hiding desktop content until floating
@@ -205,12 +206,10 @@ export function ComboboxItem({
{selected || trailingSlot ? (
+
+ {selected ? : null}
+
{trailingSlot}
- {selected ? (
-
-
-
- ) : null}
) : null}
@@ -244,6 +243,7 @@ export function Combobox({
open,
onOpenChange,
enableDismissOnClose,
+ stackBehavior,
desktopPlacement = "top-start",
desktopPreventInitialFlash = true,
anchorRef,
@@ -653,6 +653,7 @@ export function Combobox({
backdropComponent={renderBackdrop}
enablePanDownToClose
enableDismissOnClose={enableDismissOnClose}
+ stackBehavior={stackBehavior}
backgroundComponent={ComboboxSheetBackground}
handleIndicatorStyle={styles.bottomSheetHandle}
keyboardBehavior="extend"