diff --git a/packages/app/src/app/index.tsx b/packages/app/src/app/index.tsx index f63f0c627..c95743b2a 100644 --- a/packages/app/src/app/index.tsx +++ b/packages/app/src/app/index.tsx @@ -119,7 +119,6 @@ export default function HomeScreen() { modelError, refreshProviderModels, persistFormPreferences, - userEditedPreferencesRef, } = useAgentFormState({ initialServerId: resolvedServerId ?? null, initialValues, @@ -139,11 +138,8 @@ export default function HomeScreen() { if (!isValidModel) { return; } - if (userEditedPreferencesRef.current.model) { - return; - } setModelFromUser(resolvedModel); - }, [availableModels, resolvedModel, setModelFromUser, userEditedPreferencesRef]); + }, [availableModels, resolvedModel, setModelFromUser]); const hostEntry = selectedServerId ? connectionStates.get(selectedServerId) : undefined; diff --git a/packages/app/src/hooks/use-agent-form-state.ts b/packages/app/src/hooks/use-agent-form-state.ts index 162baa884..1d9fff726 100644 --- a/packages/app/src/hooks/use-agent-form-state.ts +++ b/packages/app/src/hooks/use-agent-form-state.ts @@ -1,5 +1,4 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import AsyncStorage from "@react-native-async-storage/async-storage"; import { AGENT_PROVIDER_DEFINITIONS, type AgentProviderDefinition, @@ -13,6 +12,7 @@ import type { WSInboundMessage } from "@server/server/messages"; import type { ProviderModelState } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store"; import { generateMessageId } from "@/types/stream"; +import { useFormPreferences } from "./use-form-preferences"; export type CreateAgentInitialValues = { workingDir?: string; @@ -42,13 +42,6 @@ type UseAgentFormStateResult = { workingDir: string; setWorkingDir: (value: string) => void; setWorkingDirFromUser: (value: string) => void; - userEditedPreferencesRef: React.MutableRefObject<{ - provider: boolean; - mode: boolean; - model: boolean; - workingDir: boolean; - serverId: boolean; - }>; providerDefinitions: AgentProviderDefinition[]; providerDefinitionMap: Map; agentDefinition?: AgentProviderDefinition; @@ -66,8 +59,6 @@ type UseAgentFormStateResult = { persistFormPreferences: () => Promise; }; -const FORM_PREFERENCES_STORAGE_KEY = "@paseo:create-agent-preferences"; - const providerDefinitions = AGENT_PROVIDER_DEFINITIONS; const providerDefinitionMap = new Map( providerDefinitions.map((definition) => [definition.id, definition]) @@ -88,6 +79,14 @@ export function useAgentFormState( isTargetDaemonReady = true, } = options; + const { + preferences, + isLoading: isPreferencesLoading, + getProviderPreferences, + updatePreferences, + updateProviderPreferences, + } = useFormPreferences(); + const [selectedServerId, setSelectedServerId] = useState( initialServerId ); @@ -99,14 +98,7 @@ export function useAgentFormState( ); const [selectedModel, setSelectedModel] = useState(""); - const formPreferencesHydratedRef = useRef(false); - const userEditedPreferencesRef = useRef({ - provider: false, - mode: false, - model: false, - workingDir: false, - serverId: false, - }); + const hasHydratedRef = useRef(false); const hasAppliedInitialValuesRef = useRef(false); const providerModelRequestTimersRef = useRef< Map> @@ -121,34 +113,52 @@ export function useAgentFormState( [] ); - const setSelectedServerIdFromUser = useCallback((value: string | null) => { - userEditedPreferencesRef.current.serverId = true; - setSelectedServerId(value); - }, []); + const setSelectedServerIdFromUser = useCallback( + (value: string | null) => { + setSelectedServerId(value); + void updatePreferences({ serverId: value ?? undefined }); + }, + [updatePreferences] + ); - const setProviderFromUser = useCallback((provider: AgentProvider) => { - userEditedPreferencesRef.current.provider = true; - setSelectedProvider(provider); - userEditedPreferencesRef.current.model = true; - setSelectedModel(""); - userEditedPreferencesRef.current.mode = true; - setSelectedMode(""); - }, []); + const setProviderFromUser = useCallback( + (provider: AgentProvider) => { + setSelectedProvider(provider); + void updatePreferences({ provider }); - const setModeFromUser = useCallback((modeId: string) => { - userEditedPreferencesRef.current.mode = true; - setSelectedMode(modeId); - }, []); + // Restore per-provider preferences if available + const providerPrefs = getProviderPreferences(provider); + const providerDef = providerDefinitionMap.get(provider); - const setModelFromUser = useCallback((modelId: string) => { - userEditedPreferencesRef.current.model = true; - setSelectedModel(modelId); - }, []); + setSelectedModel(providerPrefs?.model ?? ""); + setSelectedMode(providerPrefs?.mode ?? providerDef?.defaultModeId ?? ""); + }, + [getProviderPreferences, updatePreferences] + ); - const setWorkingDirFromUser = useCallback((value: string) => { - userEditedPreferencesRef.current.workingDir = true; - setWorkingDir(value); - }, []); + const setModeFromUser = useCallback( + (modeId: string) => { + setSelectedMode(modeId); + void updateProviderPreferences(selectedProvider, { mode: modeId }); + }, + [selectedProvider, updateProviderPreferences] + ); + + const setModelFromUser = useCallback( + (modelId: string) => { + setSelectedModel(modelId); + void updateProviderPreferences(selectedProvider, { model: modelId }); + }, + [selectedProvider, updateProviderPreferences] + ); + + const setWorkingDirFromUser = useCallback( + (value: string) => { + setWorkingDir(value); + void updatePreferences({ workingDir: value }); + }, + [updatePreferences] + ); const applyInitialValues = useCallback(() => { if (!isCreateFlow || !initialValues) { @@ -156,23 +166,18 @@ export function useAgentFormState( } if (Object.prototype.hasOwnProperty.call(initialValues, "workingDir")) { - const providedWorkingDir = initialValues.workingDir ?? ""; - userEditedPreferencesRef.current.workingDir = true; - setWorkingDir(providedWorkingDir); + setWorkingDir(initialValues.workingDir ?? ""); } if (initialValues.provider && providerDefinitionMap.has(initialValues.provider)) { - userEditedPreferencesRef.current.provider = true; setSelectedProvider(initialValues.provider); } if (typeof initialValues.modeId === "string" && initialValues.modeId.length > 0) { - userEditedPreferencesRef.current.mode = true; setSelectedMode(initialValues.modeId); } if (typeof initialValues.model === "string" && initialValues.model.length > 0) { - userEditedPreferencesRef.current.model = true; setSelectedModel(initialValues.model); } }, [initialValues, isCreateFlow]); @@ -199,163 +204,46 @@ export function useAgentFormState( }); }, [requestProviderModels, selectedProvider, workingDir]); + // Hydrate form state from preferences once loaded useEffect(() => { - let isActive = true; - const hydratePreferences = async () => { - try { - const stored = await AsyncStorage.getItem(FORM_PREFERENCES_STORAGE_KEY); - if (!stored || !isActive) { - return; - } - const parsed = JSON.parse(stored) as { - workingDir?: string; - provider?: AgentProvider; - mode?: string; - model?: string; - serverId?: string; - }; - if ( - parsed.provider && - providerDefinitionMap.has(parsed.provider) && - !userEditedPreferencesRef.current.provider - ) { - setSelectedProvider(parsed.provider); - } - if (typeof parsed.mode === "string" && !userEditedPreferencesRef.current.mode) { - setSelectedMode(parsed.mode); - } - if ( - typeof parsed.workingDir === "string" && - !userEditedPreferencesRef.current.workingDir - ) { - setWorkingDir(parsed.workingDir); - } - if (typeof parsed.model === "string" && !userEditedPreferencesRef.current.model) { - setSelectedModel(parsed.model); - } - if ( - typeof parsed.serverId === "string" && - !userEditedPreferencesRef.current.serverId - ) { - setSelectedServerId(parsed.serverId); - } - } catch (error) { - console.error( - "[useAgentFormState] Failed to hydrate form preferences:", - error - ); - } finally { - if (isActive) { - formPreferencesHydratedRef.current = true; - } - } - }; - void hydratePreferences(); - return () => { - isActive = false; - }; - }, []); + if (isPreferencesLoading || hasHydratedRef.current) return; + hasHydratedRef.current = true; - useEffect(() => { - if (!formPreferencesHydratedRef.current) { - return; - } - const persist = async () => { - const nextPayload: { - workingDir?: string; - provider?: AgentProvider; - mode?: string; - model?: string; - serverId?: string; - } = {}; - if (userEditedPreferencesRef.current.workingDir) { - nextPayload.workingDir = workingDir; - } - if (userEditedPreferencesRef.current.provider) { - nextPayload.provider = selectedProvider; - } - if (userEditedPreferencesRef.current.mode) { - nextPayload.mode = selectedMode; - } - if (userEditedPreferencesRef.current.model) { - nextPayload.model = selectedModel; - } - if (userEditedPreferencesRef.current.serverId) { - nextPayload.serverId = selectedServerId ?? undefined; - } - if (Object.keys(nextPayload).length === 0) { - return; - } - try { - const stored = await AsyncStorage.getItem(FORM_PREFERENCES_STORAGE_KEY); - const parsed = stored - ? (JSON.parse(stored) as Record) - : {}; - await AsyncStorage.setItem( - FORM_PREFERENCES_STORAGE_KEY, - JSON.stringify({ - ...parsed, - ...nextPayload, - }) - ); - } catch (error) { - console.error( - "[useAgentFormState] Failed to persist form preferences:", - error - ); - } - }; - void persist(); - }, [selectedMode, selectedProvider, workingDir, selectedModel, selectedServerId]); + const activeProvider = + preferences.provider && + providerDefinitionMap.has(preferences.provider as AgentProvider) + ? (preferences.provider as AgentProvider) + : DEFAULT_PROVIDER; + + const providerPrefs = preferences.providerPreferences?.[activeProvider]; + const providerDef = providerDefinitionMap.get(activeProvider); + + setSelectedProvider(activeProvider); + if (preferences.workingDir) setWorkingDir(preferences.workingDir); + setSelectedMode(providerPrefs?.mode ?? providerDef?.defaultModeId ?? ""); + if (providerPrefs?.model) setSelectedModel(providerPrefs.model); + if (preferences.serverId) setSelectedServerId(preferences.serverId); + }, [isPreferencesLoading, preferences]); const persistFormPreferences = useCallback(async () => { - const payload: { - workingDir?: string; - provider?: AgentProvider; - mode?: string; - model?: string; - serverId?: string; - } = {}; - - if (typeof workingDir === "string") { - payload.workingDir = workingDir; - } - if (selectedProvider) { - payload.provider = selectedProvider; - } - if (typeof selectedMode === "string") { - payload.mode = selectedMode; - } - if (typeof selectedModel === "string") { - payload.model = selectedModel; - } - if (selectedServerId) { - payload.serverId = selectedServerId; - } - - if (Object.keys(payload).length === 0) { - return; - } - - try { - const stored = await AsyncStorage.getItem(FORM_PREFERENCES_STORAGE_KEY); - const parsed = stored - ? (JSON.parse(stored) as Record) - : {}; - await AsyncStorage.setItem( - FORM_PREFERENCES_STORAGE_KEY, - JSON.stringify({ - ...parsed, - ...payload, - }) - ); - } catch (error) { - console.error( - "[useAgentFormState] Failed to persist form preferences:", - error - ); - } - }, [selectedMode, selectedModel, selectedProvider, selectedServerId, workingDir]); + await updatePreferences({ + workingDir, + provider: selectedProvider, + serverId: selectedServerId ?? undefined, + }); + await updateProviderPreferences(selectedProvider, { + mode: selectedMode, + model: selectedModel, + }); + }, [ + selectedMode, + selectedModel, + selectedProvider, + selectedServerId, + workingDir, + updatePreferences, + updateProviderPreferences, + ]); const clearQueuedProviderModelRequest = useCallback((serverId: string | null) => { if (!serverId) { @@ -489,7 +377,6 @@ export function useAgentFormState( workingDir, setWorkingDir, setWorkingDirFromUser, - userEditedPreferencesRef, providerDefinitions, providerDefinitionMap, agentDefinition, @@ -520,6 +407,7 @@ export function useAgentFormState( setProviderFromUser, setModeFromUser, setModelFromUser, + setWorkingDirFromUser, workingDir, workingDirIsEmpty, persistFormPreferences, diff --git a/packages/app/src/hooks/use-form-preferences.ts b/packages/app/src/hooks/use-form-preferences.ts new file mode 100644 index 000000000..8edcf201c --- /dev/null +++ b/packages/app/src/hooks/use-form-preferences.ts @@ -0,0 +1,103 @@ +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"]; + +const providerPreferencesSchema = z.object({ + model: z.string().optional(), + mode: z.string().optional(), +}); + +const formPreferencesSchema = z.object({ + workingDir: z.string().optional(), + provider: z.string().optional(), + serverId: z.string().optional(), + providerPreferences: z.record(providerPreferencesSchema).optional(), +}); + +export type ProviderPreferences = z.infer; +export type FormPreferences = z.infer; + +const DEFAULT_FORM_PREFERENCES: FormPreferences = {}; + +async function loadFormPreferences(): Promise { + const stored = await AsyncStorage.getItem(FORM_PREFERENCES_STORAGE_KEY); + if (!stored) return DEFAULT_FORM_PREFERENCES; + const result = formPreferencesSchema.safeParse(JSON.parse(stored)); + return result.success ? result.data : DEFAULT_FORM_PREFERENCES; +} + +export interface UseFormPreferencesReturn { + preferences: FormPreferences; + isLoading: boolean; + getProviderPreferences: (provider: AgentProvider) => ProviderPreferences | undefined; + updatePreferences: (updates: Partial) => Promise; + updateProviderPreferences: ( + provider: AgentProvider, + updates: Partial + ) => Promise; +} + +export function useFormPreferences(): UseFormPreferencesReturn { + const queryClient = useQueryClient(); + const { data, isPending } = useQuery({ + queryKey: FORM_PREFERENCES_QUERY_KEY, + queryFn: loadFormPreferences, + staleTime: Infinity, + gcTime: Infinity, + }); + + const preferences = data ?? DEFAULT_FORM_PREFERENCES; + + const getProviderPreferences = useCallback( + (provider: AgentProvider): ProviderPreferences | undefined => { + return preferences.providerPreferences?.[provider]; + }, + [preferences.providerPreferences] + ); + + const updatePreferences = useCallback( + async (updates: Partial) => { + const prev = + queryClient.getQueryData(FORM_PREFERENCES_QUERY_KEY) ?? + DEFAULT_FORM_PREFERENCES; + const next = { ...prev, ...updates }; + queryClient.setQueryData(FORM_PREFERENCES_QUERY_KEY, next); + await AsyncStorage.setItem(FORM_PREFERENCES_STORAGE_KEY, JSON.stringify(next)); + }, + [queryClient] + ); + + const updateProviderPreferences = useCallback( + async (provider: AgentProvider, updates: Partial) => { + const prev = + queryClient.getQueryData(FORM_PREFERENCES_QUERY_KEY) ?? + DEFAULT_FORM_PREFERENCES; + const next: FormPreferences = { + ...prev, + providerPreferences: { + ...prev.providerPreferences, + [provider]: { + ...prev.providerPreferences?.[provider], + ...updates, + }, + }, + }; + queryClient.setQueryData(FORM_PREFERENCES_QUERY_KEY, next); + await AsyncStorage.setItem(FORM_PREFERENCES_STORAGE_KEY, JSON.stringify(next)); + }, + [queryClient] + ); + + return { + preferences, + isLoading: isPending, + getProviderPreferences, + updatePreferences, + updateProviderPreferences, + }; +}