From 04b21f089c652ccb9049bbe3104e7a63a90fe155 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 29 Mar 2026 21:17:31 +0700 Subject: [PATCH] Fix mobile dropdown close and draft thinking persistence Closes #150 Closes #151 --- .../app/src/components/agent-status-bar.tsx | 30 ++++++++++ .../app/src/components/ui/dropdown-menu.tsx | 5 +- .../app/src/hooks/use-agent-form-state.ts | 34 ++++++----- .../src/hooks/use-form-preferences.test.ts | 57 +++++++++++++++++++ .../app/src/hooks/use-form-preferences.ts | 31 ++++++++++ 5 files changed, 138 insertions(+), 19 deletions(-) create mode 100644 packages/app/src/hooks/use-form-preferences.test.ts diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx index f731f3102..fbefa2f45 100644 --- a/packages/app/src/components/agent-status-bar.tsx +++ b/packages/app/src/components/agent-status-bar.tsx @@ -8,6 +8,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 { mergeProviderPreferences, useFormPreferences } from "@/hooks/use-form-preferences"; import { DropdownMenu, DropdownMenuContent, @@ -609,6 +610,7 @@ function ControlledStatusBar({ const EMPTY_MODES: AgentMode[] = []; export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { + const { preferences, updatePreferences } = useFormPreferences(); const agent = useSessionStore( useShallow((state) => { const currentAgent = state.sessions[serverId]?.agents?.get(agentId) ?? null; @@ -709,6 +711,17 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { if (!client) { return; } + void updatePreferences( + mergeProviderPreferences({ + preferences, + provider: agent.provider, + updates: { + model: modelId, + }, + }), + ).catch((error) => { + console.warn("[AgentStatusBar] persist model preference failed", error); + }); void client.setAgentModel(agentId, modelId).catch((error) => { console.warn("[AgentStatusBar] setAgentModel failed", error); }); @@ -719,6 +732,23 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { if (!client) { return; } + const activeModelId = modelSelection.activeModelId; + if (activeModelId) { + void updatePreferences( + mergeProviderPreferences({ + preferences, + provider: agent.provider, + updates: { + model: activeModelId, + thinkingByModel: { + [activeModelId]: thinkingOptionId, + }, + }, + }), + ).catch((error) => { + console.warn("[AgentStatusBar] persist thinking preference failed", error); + }); + } void client.setAgentThinkingOption(agentId, thinkingOptionId).catch((error) => { console.warn("[AgentStatusBar] setAgentThinkingOption failed", error); }); diff --git a/packages/app/src/components/ui/dropdown-menu.tsx b/packages/app/src/components/ui/dropdown-menu.tsx index 0edc81cf7..9ba84fca0 100644 --- a/packages/app/src/components/ui/dropdown-menu.tsx +++ b/packages/app/src/components/ui/dropdown-menu.tsx @@ -278,7 +278,10 @@ export function DropdownMenuContent({ setModalVisible(true); setClosing(false); } else if (modalVisible) { - setClosing(true); + // Avoid leaving an invisible full-screen Modal mounted on native when + // the exit animation callback does not fire. + setClosing(false); + setModalVisible(false); } }, [open, modalVisible]); diff --git a/packages/app/src/hooks/use-agent-form-state.ts b/packages/app/src/hooks/use-agent-form-state.ts index 01d611712..7a783b61e 100644 --- a/packages/app/src/hooks/use-agent-form-state.ts +++ b/packages/app/src/hooks/use-agent-form-state.ts @@ -13,6 +13,7 @@ import { useHosts } from "@/runtime/host-runtime"; import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; import { useFormPreferences, + mergeProviderPreferences, type FormPreferences, type ProviderPreferences, } from "./use-form-preferences"; @@ -693,26 +694,23 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg const persistFormPreferences = useCallback(async () => { const resolvedModel = resolveEffectiveModel(availableModels, formState.model); const modelId = resolvedModel?.id ?? formState.model; - const existingProviderPrefs = preferences?.providerPreferences?.[formState.provider]; - - const nextProviderPrefs: ProviderPreferences = { - model: modelId || undefined, - mode: formState.modeId || undefined, - thinkingByModel: { - ...existingProviderPrefs?.thinkingByModel, - ...(modelId && formState.thinkingOptionId - ? { [modelId]: formState.thinkingOptionId } - : {}), - }, - }; - - await updatePreferences({ + const nextPreferences = mergeProviderPreferences({ + preferences: preferences ?? {}, provider: formState.provider, - providerPreferences: { - ...preferences?.providerPreferences, - [formState.provider]: nextProviderPrefs, - }, + updates: { + model: modelId || undefined, + mode: formState.modeId || undefined, + ...(modelId && formState.thinkingOptionId + ? { + thinkingByModel: { + [modelId]: formState.thinkingOptionId, + }, + } + : {}), + } satisfies Partial, }); + + await updatePreferences(nextPreferences); }, [ availableModels, formState.model, diff --git a/packages/app/src/hooks/use-form-preferences.test.ts b/packages/app/src/hooks/use-form-preferences.test.ts new file mode 100644 index 000000000..da73e4be2 --- /dev/null +++ b/packages/app/src/hooks/use-form-preferences.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "vitest"; + +import { mergeProviderPreferences } from "./use-form-preferences"; + +describe("mergeProviderPreferences", () => { + it("stores the selected model for a provider", () => { + expect( + mergeProviderPreferences({ + preferences: {}, + provider: "claude", + updates: { model: "claude-opus-4-6" }, + }), + ).toEqual({ + provider: "claude", + providerPreferences: { + claude: { + model: "claude-opus-4-6", + }, + }, + }); + }); + + it("merges thinking preferences by model without dropping existing entries", () => { + expect( + mergeProviderPreferences({ + preferences: { + provider: "claude", + providerPreferences: { + claude: { + model: "claude-sonnet-4-6", + thinkingByModel: { + "claude-sonnet-4-6": "medium", + }, + }, + }, + }, + provider: "claude", + updates: { + thinkingByModel: { + "claude-opus-4-6": "high", + }, + }, + }), + ).toEqual({ + provider: "claude", + providerPreferences: { + claude: { + model: "claude-sonnet-4-6", + thinkingByModel: { + "claude-sonnet-4-6": "medium", + "claude-opus-4-6": "high", + }, + }, + }, + }); + }); +}); diff --git a/packages/app/src/hooks/use-form-preferences.ts b/packages/app/src/hooks/use-form-preferences.ts index 5e5dc0363..6031e16bc 100644 --- a/packages/app/src/hooks/use-form-preferences.ts +++ b/packages/app/src/hooks/use-form-preferences.ts @@ -2,6 +2,7 @@ import { useCallback } from "react"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { z } from "zod"; +import type { AgentProvider } from "@server/server/agent/agent-sdk-types"; const FORM_PREFERENCES_STORAGE_KEY = "@paseo:create-agent-preferences"; const FORM_PREFERENCES_QUERY_KEY = ["form-preferences"]; @@ -35,6 +36,36 @@ export interface UseFormPreferencesReturn { updatePreferences: (updates: Partial) => Promise; } +export function mergeProviderPreferences(args: { + preferences: FormPreferences; + provider: AgentProvider; + updates: Partial; +}): FormPreferences { + const { preferences, provider, updates } = args; + const existingProviderPreferences = preferences.providerPreferences ?? {}; + const existing = existingProviderPreferences[provider] ?? {}; + const nextThinkingByModel = + updates.thinkingByModel === undefined + ? existing.thinkingByModel + : { + ...existing.thinkingByModel, + ...updates.thinkingByModel, + }; + + return { + ...preferences, + provider, + providerPreferences: { + ...existingProviderPreferences, + [provider]: { + ...existing, + ...updates, + ...(nextThinkingByModel ? { thinkingByModel: nextThinkingByModel } : {}), + }, + }, + }; +} + export function useFormPreferences(): UseFormPreferencesReturn { const queryClient = useQueryClient(); const { data, isPending } = useQuery({