diff --git a/packages/app/src/components/combined-model-selector.tsx b/packages/app/src/components/combined-model-selector.tsx index 9c87318b8..ad14b64a7 100644 --- a/packages/app/src/components/combined-model-selector.tsx +++ b/packages/app/src/components/combined-model-selector.tsx @@ -23,7 +23,6 @@ import type { AgentProvider } from "@getpaseo/protocol/agent-types"; import type { SheetHeader } from "@/components/adaptive-modal-sheet"; import { useProviderSettingsStore } from "@/stores/provider-settings-store"; import { Button } from "@/components/ui/button"; -import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; const IS_WEB = platformIsWeb; import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox"; @@ -66,6 +65,10 @@ import { // TODO: this should be configured per provider in the provider manifest const PROVIDERS_WITH_MODEL_DESCRIPTIONS = new Set(["opencode", "pi"]); +const DESKTOP_PROVIDER_VIEW_MIN_HEIGHT = 220; +const DESKTOP_PROVIDER_VIEW_MAX_HEIGHT = 400; +const DESKTOP_PROVIDER_VIEW_BASE_HEIGHT = 80; +const DESKTOP_MODEL_ROW_HEIGHT = 40; type SelectorView = | { kind: "all" } @@ -87,6 +90,8 @@ interface CombinedModelSelectorProps { }) => React.ReactNode; onOpen?: () => void; onClose?: () => void; + onRetryProvider?: (provider: AgentProvider) => void; + isRetryingProvider?: boolean; disabled?: boolean; serverId?: string | null; } @@ -101,7 +106,8 @@ interface SelectorContentProps { onSelect: (provider: string, modelId: string) => void; onToggleFavorite?: (provider: string, modelId: string) => void; onDrillDown: (providerId: string, providerLabel: string) => void; - serverId: string | null; + onRetryProvider?: (provider: AgentProvider) => void; + isRetryingProvider: boolean; } function normalizeSearchQuery(value: string): string { @@ -418,26 +424,29 @@ function ProviderModelRows({ } function ProviderErrorEmptyState({ - serverId, providerId, message, + onRetryProvider, + isRetryingProvider, }: { - serverId: string | null; providerId: string; message: string; + onRetryProvider?: (provider: AgentProvider) => void; + isRetryingProvider: boolean; }) { const { theme } = useUnistyles(); - const { refresh, isRefreshing } = useProvidersSnapshot(serverId); const handleRetry = useCallback(() => { - void refresh([providerId]); - }, [refresh, providerId]); + onRetryProvider?.(providerId); + }, [onRetryProvider, providerId]); return ( {message} - + {onRetryProvider ? ( + + ) : null} ); } @@ -452,7 +461,8 @@ function SelectorContent({ onSelect, onToggleFavorite, onDrillDown, - serverId, + onRetryProvider, + isRetryingProvider, }: SelectorContentProps) { const { theme } = useUnistyles(); const normalizedQuery = useMemo(() => normalizeSearchQuery(searchQuery), [searchQuery]); @@ -502,9 +512,10 @@ function SelectorContent({ if (drillSelection.kind === "error") { return ( ); } @@ -556,6 +567,8 @@ export function CombinedModelSelector({ renderTrigger, onOpen, onClose, + onRetryProvider, + isRetryingProvider = false, disabled = false, serverId = null, }: CombinedModelSelectorProps) { @@ -630,8 +643,17 @@ export function CombinedModelSelector({ return undefined; } const provider = providers.find((entry) => entry.id === view.providerId); - const modelCount = provider ? getProviderModelRows(provider).length : 0; - return Math.min(80 + modelCount * 40, 400); + if (!provider || provider.modelSelection.kind !== "models") { + return DESKTOP_PROVIDER_VIEW_MIN_HEIGHT; + } + const modelCount = getProviderModelRows(provider).length; + return Math.min( + Math.max( + DESKTOP_PROVIDER_VIEW_MIN_HEIGHT, + DESKTOP_PROVIDER_VIEW_BASE_HEIGHT + modelCount * DESKTOP_MODEL_ROW_HEIGHT, + ), + DESKTOP_PROVIDER_VIEW_MAX_HEIGHT, + ); }, [providers, view]); const triggerLabel = useMemo(() => { @@ -804,7 +826,8 @@ export function CombinedModelSelector({ onSelect={handleSelect} onToggleFavorite={onToggleFavorite} onDrillDown={handleDrillDown} - serverId={serverId} + onRetryProvider={onRetryProvider} + isRetryingProvider={isRetryingProvider} /> ) : ( diff --git a/packages/app/src/composer/agent-controls/index.tsx b/packages/app/src/composer/agent-controls/index.tsx index dd2733aff..39b2375d3 100644 --- a/packages/app/src/composer/agent-controls/index.tsx +++ b/packages/app/src/composer/agent-controls/index.tsx @@ -24,6 +24,7 @@ import { getProviderIcon } from "@/components/provider-icons"; import { CombinedModelSelector } from "@/components/combined-model-selector"; import { buildProviderSelectorProviders, + buildSelectableProviderSelectorProviders, type ProviderSelectorProvider, } from "@/provider-selection/provider-selection"; import { useSessionStore } from "@/stores/session-store"; @@ -91,6 +92,8 @@ interface ControlledAgentControlsProps { onSetFeature?: (featureId: string, value: unknown) => void; onDropdownClose?: () => void; onModelSelectorOpen?: () => void; + onRetryModelProvider?: (provider: AgentProvider) => void; + isRetryingModelProvider?: boolean; /** Extra elements rendered inline with the agent controls (desktop only). */ desktopExtras?: ReactNode; modelSelectorServerId?: string | null; @@ -117,6 +120,8 @@ export interface DraftAgentControlsProps { onSetFeature?: (featureId: string, value: unknown) => void; onDropdownClose?: () => void; onModelSelectorOpen?: () => void; + onRetryModelProvider?: (provider: AgentProvider) => void; + isRetryingModelProvider?: boolean; disabled?: boolean; modelSelectorServerId?: string | null; } @@ -175,17 +180,6 @@ function getFeatureIconColor( } } -function resolveDisplayModel( - isModelLoading: boolean, - modelOptions: AgentControlOption[] | undefined, - selectedModelId: string | undefined, -) { - if (isModelLoading && (!modelOptions || modelOptions.length === 0)) { - return "Loading models..."; - } - return findOptionLabel(modelOptions, selectedModelId, "Select model"); -} - // Mobile agent controls only — strip namespace prefix so providers like OpenCode // show "gpt-5.5" instead of "openrouter/gpt-5.5". Full label still appears in // the model picker. @@ -411,6 +405,8 @@ function ControlledAgentControls({ onSetFeature, onDropdownClose, onModelSelectorOpen, + onRetryModelProvider, + isRetryingModelProvider = false, desktopExtras, modelSelectorServerId = null, }: ControlledAgentControlsProps) { @@ -432,7 +428,6 @@ function ControlledAgentControls({ ); const displayProvider = findOptionLabel(providerOptions, selectedProviderId, "Provider"); - const displayModel = resolveDisplayModel(isModelLoading, modelOptions, selectedModelId); const formattedThinkingOptions = useMemo( () => toThinkingControlOptions(thinkingOptions), [thinkingOptions], @@ -588,6 +583,8 @@ function ControlledAgentControls({ onToggleFavoriteModel={onToggleFavoriteModel} onDropdownClose={onDropdownClose} onModelSelectorOpen={onModelSelectorOpen} + onRetryModelProvider={onRetryModelProvider} + isRetryingModelProvider={isRetryingModelProvider} favoriteKeys={favoriteKeys} disabled={disabled} isModelLoading={isModelLoading} @@ -599,7 +596,6 @@ function ControlledAgentControls({ comboboxProviderOptions={comboboxProviderOptions} comboboxThinkingOptions={comboboxThinkingOptions} displayProvider={displayProvider} - displayModel={displayModel} displayThinking={displayThinking} openSelector={openSelector} providerAnchorRef={providerAnchorRef} @@ -628,6 +624,8 @@ function ControlledAgentControls({ onToggleFavoriteModel={onToggleFavoriteModel} onDropdownClose={onDropdownClose} onModelSelectorOpen={onModelSelectorOpen} + onRetryModelProvider={onRetryModelProvider} + isRetryingModelProvider={isRetryingModelProvider} favoriteKeys={favoriteKeys} disabled={disabled} isModelLoading={isModelLoading} @@ -665,6 +663,8 @@ interface DesktopAgentControlsContentProps { onToggleFavoriteModel?: (provider: string, modelId: string) => void; onDropdownClose?: () => void; onModelSelectorOpen?: () => void; + onRetryModelProvider?: (provider: AgentProvider) => void; + isRetryingModelProvider: boolean; favoriteKeys: Set; disabled: boolean; isModelLoading: boolean; @@ -676,7 +676,6 @@ interface DesktopAgentControlsContentProps { comboboxProviderOptions: ComboboxOption[]; comboboxThinkingOptions: ComboboxOption[]; displayProvider: string; - displayModel: string; displayThinking: string; openSelector: AgentControlSelector | null; providerAnchorRef: RefObject; @@ -717,6 +716,8 @@ function DesktopAgentControlsContent(props: DesktopAgentControlsContentProps) { onToggleFavoriteModel, onDropdownClose, onModelSelectorOpen, + onRetryModelProvider, + isRetryingModelProvider, favoriteKeys, disabled, isModelLoading, @@ -728,7 +729,6 @@ function DesktopAgentControlsContent(props: DesktopAgentControlsContentProps) { comboboxProviderOptions, comboboxThinkingOptions, displayProvider, - displayModel, displayThinking, openSelector, providerAnchorRef, @@ -779,12 +779,7 @@ function DesktopAgentControlsContent(props: DesktopAgentControlsContentProps) { ) : null} {canSelectModel ? ( - + @@ -870,6 +867,8 @@ interface SheetAgentControlsContentProps { onToggleFavoriteModel?: (provider: string, modelId: string) => void; onDropdownClose?: () => void; onModelSelectorOpen?: () => void; + onRetryModelProvider?: (provider: AgentProvider) => void; + isRetryingModelProvider: boolean; favoriteKeys: Set; disabled: boolean; isModelLoading: boolean; @@ -906,6 +905,8 @@ function SheetAgentControlsContent(props: SheetAgentControlsContentProps) { onToggleFavoriteModel, onDropdownClose, onModelSelectorOpen, + onRetryModelProvider, + isRetryingModelProvider, favoriteKeys, disabled, isModelLoading, @@ -992,6 +993,8 @@ function SheetAgentControlsContent(props: SheetAgentControlsContentProps) { disabled={modelDisabled} onOpen={onModelSelectorOpen} onClose={onDropdownClose} + onRetryProvider={onRetryModelProvider} + isRetryingProvider={isRetryingModelProvider} renderTrigger={renderModelTrigger} serverId={modelSelectorServerId} /> @@ -1353,6 +1356,8 @@ export const AgentControls = memo(function AgentControls({ const { entries: snapshotEntries, isLoading: snapshotIsLoading, + isRefreshing: snapshotIsRefreshing, + refresh: refreshSnapshot, refetchIfStale: refetchSnapshotIfStale, } = useProvidersSnapshot(serverId, { cwd: agent?.cwd }); @@ -1373,14 +1378,15 @@ export const AgentControls = memo(function AgentControls({ () => buildAgentProviderModels(agent?.provider, models), [agent?.provider, models], ); - const agentModelSelectorProviders = useMemo( - () => - buildProviderSelectorProviders({ - providerDefinitions: agentProviderDefinitions, - modelsByProvider: agentProviderModels, - }), - [agentProviderDefinitions, agentProviderModels], - ); + const agentModelSelectorProviders = useMemo(() => { + if (snapshotSelectedEntry) { + return buildSelectableProviderSelectorProviders([snapshotSelectedEntry]); + } + return buildProviderSelectorProviders({ + providerDefinitions: agentProviderDefinitions, + modelsByProvider: agentProviderModels, + }); + }, [agentProviderDefinitions, agentProviderModels, snapshotSelectedEntry]); const modelSelection = resolveAgentModelSelection({ models, @@ -1504,6 +1510,13 @@ export const AgentControls = memo(function AgentControls({ refetchSnapshotIfStale(agentProvider); }, [agentProvider, refetchSnapshotIfStale]); + const handleRetryModelProvider = useCallback( + (provider: AgentProvider) => { + void refreshSnapshot([provider]); + }, + [refreshSnapshot], + ); + const modeChip = useMemo( () => , [serverId, agentId], @@ -1529,6 +1542,8 @@ export const AgentControls = memo(function AgentControls({ onSetFeature={handleSetFeature} isModelLoading={snapshotIsLoading || selectedProviderIsLoading} onModelSelectorOpen={handleModelSelectorOpen} + onRetryModelProvider={handleRetryModelProvider} + isRetryingModelProvider={snapshotIsRefreshing} onDropdownClose={onDropdownClose} disabled={!client} desktopExtras={modeChip} @@ -1558,6 +1573,8 @@ export function DraftAgentControls({ onSetFeature, onDropdownClose, onModelSelectorOpen, + onRetryModelProvider, + isRetryingModelProvider = false, disabled = false, modelSelectorServerId = null, }: DraftAgentControlsProps) { @@ -1627,6 +1644,8 @@ export function DraftAgentControls({ disabled={disabled} onOpen={onModelSelectorOpen} onClose={onDropdownClose} + onRetryProvider={onRetryModelProvider} + isRetryingProvider={isRetryingModelProvider} serverId={modelSelectorServerId} /> {selectedProvider ? ( @@ -1638,6 +1657,8 @@ export function DraftAgentControls({ features={features} onSetFeature={onSetFeature} onDropdownClose={onDropdownClose} + onRetryModelProvider={onRetryModelProvider} + isRetryingModelProvider={isRetryingModelProvider} disabled={disabled} desktopExtras={draftModeChip} /> @@ -1663,6 +1684,8 @@ export function DraftAgentControls({ features={features} onSetFeature={onSetFeature} onModelSelectorOpen={onModelSelectorOpen} + onRetryModelProvider={onRetryModelProvider} + isRetryingModelProvider={isRetryingModelProvider} disabled={disabled} modelSelectorServerId={modelSelectorServerId} /> diff --git a/packages/app/src/composer/draft/input-draft-core.ts b/packages/app/src/composer/draft/input-draft-core.ts index cbcf7b77b..ad7ce7b85 100644 --- a/packages/app/src/composer/draft/input-draft-core.ts +++ b/packages/app/src/composer/draft/input-draft-core.ts @@ -46,6 +46,8 @@ export function buildDraftAgentControls(input: { onSetFeature, onDropdownClose, onModelSelectorOpen: formState.refetchProviderModelsIfStale, + onRetryModelProvider: formState.refreshProviderModels, + isRetryingModelProvider: formState.isProviderModelsRefreshing, modelSelectorServerId: formState.selectedServerId, }; } diff --git a/packages/app/src/composer/draft/input-draft.live.test.tsx b/packages/app/src/composer/draft/input-draft.live.test.tsx index f04d469e2..c4d554ee7 100644 --- a/packages/app/src/composer/draft/input-draft.live.test.tsx +++ b/packages/app/src/composer/draft/input-draft.live.test.tsx @@ -101,6 +101,7 @@ vi.mock("@/hooks/use-agent-form-state", () => ({ }, ], isAllModelsLoading: false, + isProviderModelsRefreshing: false, availableThinkingOptions: [ { id: "medium", label: "Medium" }, { id: "high", label: "High", isDefault: true }, diff --git a/packages/app/src/hooks/use-agent-form-state.ts b/packages/app/src/hooks/use-agent-form-state.ts index 552a289f0..95812bec9 100644 --- a/packages/app/src/hooks/use-agent-form-state.ts +++ b/packages/app/src/hooks/use-agent-form-state.ts @@ -69,10 +69,11 @@ export interface UseAgentFormStateResult { allProviderModels: Map; modelSelectorProviders: ProviderSelectorProvider[]; isAllModelsLoading: boolean; + isProviderModelsRefreshing: boolean; availableThinkingOptions: NonNullable; isModelLoading: boolean; modelError: string | null; - refreshProviderModels: () => void; + refreshProviderModels: (provider?: AgentProvider) => void; refetchProviderModelsIfStale: () => void; setProviderAndModelFromUser: (provider: AgentProvider, modelId: string) => void; workingDirIsEmpty: boolean; @@ -208,6 +209,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg const { entries: snapshotEntries, isLoading: snapshotIsLoading, + isRefreshing: snapshotIsRefreshing, error: snapshotError, refresh: refreshSnapshot, refetchIfStale: refetchSnapshotIfStale, @@ -475,9 +477,12 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg dispatch({ type: "SET_SERVER_ID", value }); }, []); - const refreshProviderModels = useCallback(() => { - void refreshSnapshot(); - }, [refreshSnapshot]); + const refreshProviderModels = useCallback( + (provider?: AgentProvider) => { + void refreshSnapshot(provider ? [provider] : undefined); + }, + [refreshSnapshot], + ); const refetchProviderModelsIfStale = useCallback(() => { refetchSnapshotIfStale(reducerStateRef.current.form.provider); @@ -535,6 +540,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg allProviderModels, modelSelectorProviders, isAllModelsLoading, + isProviderModelsRefreshing: snapshotIsRefreshing, availableThinkingOptions, isModelLoading, modelError, @@ -568,6 +574,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg allProviderModels, modelSelectorProviders, isAllModelsLoading, + snapshotIsRefreshing, availableThinkingOptions, isModelLoading, modelError, diff --git a/packages/app/src/provider-selection/provider-selection.test.ts b/packages/app/src/provider-selection/provider-selection.test.ts index 984195239..648caec4f 100644 --- a/packages/app/src/provider-selection/provider-selection.test.ts +++ b/packages/app/src/provider-selection/provider-selection.test.ts @@ -259,6 +259,27 @@ describe("combined model selector data", () => { ).toBe("Default"); }); + it("keeps provider snapshot errors visible in the selected trigger label", () => { + const providers = buildSelectableProviderSelectorProviders([ + snapshotEntry({ + provider: "opencode", + label: "OpenCode", + status: "error", + error: "OpenCode app.agents timed out after 10s", + models: [], + }), + ]); + + expect( + resolveSelectedModelLabel({ + providers, + selectedProvider: "opencode", + selectedModel: "", + isLoading: false, + }), + ).toBe("Error"); + }); + it("returns observable submission readiness reasons", () => { expect( resolveSubmissionReadiness({ diff --git a/packages/app/src/provider-selection/provider-selection.ts b/packages/app/src/provider-selection/provider-selection.ts index a64bf23dd..797554e24 100644 --- a/packages/app/src/provider-selection/provider-selection.ts +++ b/packages/app/src/provider-selection/provider-selection.ts @@ -162,6 +162,9 @@ export function resolveSelectedModelLabel(input: { if (provider.modelSelection.kind === "loading") { return "Loading..."; } + if (provider.modelSelection.kind === "error") { + return "Error"; + } if (provider.modelSelection.kind !== "models") { return "Select model"; }