feat(app): extract form preferences into dedicated hook with per-provider storage

This commit is contained in:
Mohamed Boudra
2026-01-11 16:21:59 +07:00
parent ebabcc09e5
commit e57cad2660
3 changed files with 194 additions and 207 deletions
+1 -5
View File
@@ -119,7 +119,6 @@ export default function HomeScreen() {
modelError, modelError,
refreshProviderModels, refreshProviderModels,
persistFormPreferences, persistFormPreferences,
userEditedPreferencesRef,
} = useAgentFormState({ } = useAgentFormState({
initialServerId: resolvedServerId ?? null, initialServerId: resolvedServerId ?? null,
initialValues, initialValues,
@@ -139,11 +138,8 @@ export default function HomeScreen() {
if (!isValidModel) { if (!isValidModel) {
return; return;
} }
if (userEditedPreferencesRef.current.model) {
return;
}
setModelFromUser(resolvedModel); setModelFromUser(resolvedModel);
}, [availableModels, resolvedModel, setModelFromUser, userEditedPreferencesRef]); }, [availableModels, resolvedModel, setModelFromUser]);
const hostEntry = selectedServerId const hostEntry = selectedServerId
? connectionStates.get(selectedServerId) ? connectionStates.get(selectedServerId)
: undefined; : undefined;
+90 -202
View File
@@ -1,5 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { import {
AGENT_PROVIDER_DEFINITIONS, AGENT_PROVIDER_DEFINITIONS,
type AgentProviderDefinition, type AgentProviderDefinition,
@@ -13,6 +12,7 @@ import type { WSInboundMessage } from "@server/server/messages";
import type { ProviderModelState } from "@/stores/session-store"; import type { ProviderModelState } from "@/stores/session-store";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import { generateMessageId } from "@/types/stream"; import { generateMessageId } from "@/types/stream";
import { useFormPreferences } from "./use-form-preferences";
export type CreateAgentInitialValues = { export type CreateAgentInitialValues = {
workingDir?: string; workingDir?: string;
@@ -42,13 +42,6 @@ type UseAgentFormStateResult = {
workingDir: string; workingDir: string;
setWorkingDir: (value: string) => void; setWorkingDir: (value: string) => void;
setWorkingDirFromUser: (value: string) => void; setWorkingDirFromUser: (value: string) => void;
userEditedPreferencesRef: React.MutableRefObject<{
provider: boolean;
mode: boolean;
model: boolean;
workingDir: boolean;
serverId: boolean;
}>;
providerDefinitions: AgentProviderDefinition[]; providerDefinitions: AgentProviderDefinition[];
providerDefinitionMap: Map<AgentProvider, AgentProviderDefinition>; providerDefinitionMap: Map<AgentProvider, AgentProviderDefinition>;
agentDefinition?: AgentProviderDefinition; agentDefinition?: AgentProviderDefinition;
@@ -66,8 +59,6 @@ type UseAgentFormStateResult = {
persistFormPreferences: () => Promise<void>; persistFormPreferences: () => Promise<void>;
}; };
const FORM_PREFERENCES_STORAGE_KEY = "@paseo:create-agent-preferences";
const providerDefinitions = AGENT_PROVIDER_DEFINITIONS; const providerDefinitions = AGENT_PROVIDER_DEFINITIONS;
const providerDefinitionMap = new Map<AgentProvider, AgentProviderDefinition>( const providerDefinitionMap = new Map<AgentProvider, AgentProviderDefinition>(
providerDefinitions.map((definition) => [definition.id, definition]) providerDefinitions.map((definition) => [definition.id, definition])
@@ -88,6 +79,14 @@ export function useAgentFormState(
isTargetDaemonReady = true, isTargetDaemonReady = true,
} = options; } = options;
const {
preferences,
isLoading: isPreferencesLoading,
getProviderPreferences,
updatePreferences,
updateProviderPreferences,
} = useFormPreferences();
const [selectedServerId, setSelectedServerId] = useState<string | null>( const [selectedServerId, setSelectedServerId] = useState<string | null>(
initialServerId initialServerId
); );
@@ -99,14 +98,7 @@ export function useAgentFormState(
); );
const [selectedModel, setSelectedModel] = useState(""); const [selectedModel, setSelectedModel] = useState("");
const formPreferencesHydratedRef = useRef(false); const hasHydratedRef = useRef(false);
const userEditedPreferencesRef = useRef({
provider: false,
mode: false,
model: false,
workingDir: false,
serverId: false,
});
const hasAppliedInitialValuesRef = useRef(false); const hasAppliedInitialValuesRef = useRef(false);
const providerModelRequestTimersRef = useRef< const providerModelRequestTimersRef = useRef<
Map<string, ReturnType<typeof setTimeout>> Map<string, ReturnType<typeof setTimeout>>
@@ -121,34 +113,52 @@ export function useAgentFormState(
[] []
); );
const setSelectedServerIdFromUser = useCallback((value: string | null) => { const setSelectedServerIdFromUser = useCallback(
userEditedPreferencesRef.current.serverId = true; (value: string | null) => {
setSelectedServerId(value); setSelectedServerId(value);
}, []); void updatePreferences({ serverId: value ?? undefined });
},
[updatePreferences]
);
const setProviderFromUser = useCallback((provider: AgentProvider) => { const setProviderFromUser = useCallback(
userEditedPreferencesRef.current.provider = true; (provider: AgentProvider) => {
setSelectedProvider(provider); setSelectedProvider(provider);
userEditedPreferencesRef.current.model = true; void updatePreferences({ provider });
setSelectedModel("");
userEditedPreferencesRef.current.mode = true;
setSelectedMode("");
}, []);
const setModeFromUser = useCallback((modeId: string) => { // Restore per-provider preferences if available
userEditedPreferencesRef.current.mode = true; const providerPrefs = getProviderPreferences(provider);
setSelectedMode(modeId); const providerDef = providerDefinitionMap.get(provider);
}, []);
const setModelFromUser = useCallback((modelId: string) => { setSelectedModel(providerPrefs?.model ?? "");
userEditedPreferencesRef.current.model = true; setSelectedMode(providerPrefs?.mode ?? providerDef?.defaultModeId ?? "");
setSelectedModel(modelId); },
}, []); [getProviderPreferences, updatePreferences]
);
const setWorkingDirFromUser = useCallback((value: string) => { const setModeFromUser = useCallback(
userEditedPreferencesRef.current.workingDir = true; (modeId: string) => {
setWorkingDir(value); 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(() => { const applyInitialValues = useCallback(() => {
if (!isCreateFlow || !initialValues) { if (!isCreateFlow || !initialValues) {
@@ -156,23 +166,18 @@ export function useAgentFormState(
} }
if (Object.prototype.hasOwnProperty.call(initialValues, "workingDir")) { if (Object.prototype.hasOwnProperty.call(initialValues, "workingDir")) {
const providedWorkingDir = initialValues.workingDir ?? ""; setWorkingDir(initialValues.workingDir ?? "");
userEditedPreferencesRef.current.workingDir = true;
setWorkingDir(providedWorkingDir);
} }
if (initialValues.provider && providerDefinitionMap.has(initialValues.provider)) { if (initialValues.provider && providerDefinitionMap.has(initialValues.provider)) {
userEditedPreferencesRef.current.provider = true;
setSelectedProvider(initialValues.provider); setSelectedProvider(initialValues.provider);
} }
if (typeof initialValues.modeId === "string" && initialValues.modeId.length > 0) { if (typeof initialValues.modeId === "string" && initialValues.modeId.length > 0) {
userEditedPreferencesRef.current.mode = true;
setSelectedMode(initialValues.modeId); setSelectedMode(initialValues.modeId);
} }
if (typeof initialValues.model === "string" && initialValues.model.length > 0) { if (typeof initialValues.model === "string" && initialValues.model.length > 0) {
userEditedPreferencesRef.current.model = true;
setSelectedModel(initialValues.model); setSelectedModel(initialValues.model);
} }
}, [initialValues, isCreateFlow]); }, [initialValues, isCreateFlow]);
@@ -199,163 +204,46 @@ export function useAgentFormState(
}); });
}, [requestProviderModels, selectedProvider, workingDir]); }, [requestProviderModels, selectedProvider, workingDir]);
// Hydrate form state from preferences once loaded
useEffect(() => { useEffect(() => {
let isActive = true; if (isPreferencesLoading || hasHydratedRef.current) return;
const hydratePreferences = async () => { hasHydratedRef.current = true;
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;
};
}, []);
useEffect(() => { const activeProvider =
if (!formPreferencesHydratedRef.current) { preferences.provider &&
return; providerDefinitionMap.has(preferences.provider as AgentProvider)
} ? (preferences.provider as AgentProvider)
const persist = async () => { : DEFAULT_PROVIDER;
const nextPayload: {
workingDir?: string; const providerPrefs = preferences.providerPreferences?.[activeProvider];
provider?: AgentProvider; const providerDef = providerDefinitionMap.get(activeProvider);
mode?: string;
model?: string; setSelectedProvider(activeProvider);
serverId?: string; if (preferences.workingDir) setWorkingDir(preferences.workingDir);
} = {}; setSelectedMode(providerPrefs?.mode ?? providerDef?.defaultModeId ?? "");
if (userEditedPreferencesRef.current.workingDir) { if (providerPrefs?.model) setSelectedModel(providerPrefs.model);
nextPayload.workingDir = workingDir; if (preferences.serverId) setSelectedServerId(preferences.serverId);
} }, [isPreferencesLoading, preferences]);
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<string, unknown>)
: {};
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 persistFormPreferences = useCallback(async () => { const persistFormPreferences = useCallback(async () => {
const payload: { await updatePreferences({
workingDir?: string; workingDir,
provider?: AgentProvider; provider: selectedProvider,
mode?: string; serverId: selectedServerId ?? undefined,
model?: string; });
serverId?: string; await updateProviderPreferences(selectedProvider, {
} = {}; mode: selectedMode,
model: selectedModel,
if (typeof workingDir === "string") { });
payload.workingDir = workingDir; }, [
} selectedMode,
if (selectedProvider) { selectedModel,
payload.provider = selectedProvider; selectedProvider,
} selectedServerId,
if (typeof selectedMode === "string") { workingDir,
payload.mode = selectedMode; updatePreferences,
} updateProviderPreferences,
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<string, unknown>)
: {};
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]);
const clearQueuedProviderModelRequest = useCallback((serverId: string | null) => { const clearQueuedProviderModelRequest = useCallback((serverId: string | null) => {
if (!serverId) { if (!serverId) {
@@ -489,7 +377,6 @@ export function useAgentFormState(
workingDir, workingDir,
setWorkingDir, setWorkingDir,
setWorkingDirFromUser, setWorkingDirFromUser,
userEditedPreferencesRef,
providerDefinitions, providerDefinitions,
providerDefinitionMap, providerDefinitionMap,
agentDefinition, agentDefinition,
@@ -520,6 +407,7 @@ export function useAgentFormState(
setProviderFromUser, setProviderFromUser,
setModeFromUser, setModeFromUser,
setModelFromUser, setModelFromUser,
setWorkingDirFromUser,
workingDir, workingDir,
workingDirIsEmpty, workingDirIsEmpty,
persistFormPreferences, persistFormPreferences,
@@ -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<typeof providerPreferencesSchema>;
export type FormPreferences = z.infer<typeof formPreferencesSchema>;
const DEFAULT_FORM_PREFERENCES: FormPreferences = {};
async function loadFormPreferences(): Promise<FormPreferences> {
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<FormPreferences>) => Promise<void>;
updateProviderPreferences: (
provider: AgentProvider,
updates: Partial<ProviderPreferences>
) => Promise<void>;
}
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<FormPreferences>) => {
const prev =
queryClient.getQueryData<FormPreferences>(FORM_PREFERENCES_QUERY_KEY) ??
DEFAULT_FORM_PREFERENCES;
const next = { ...prev, ...updates };
queryClient.setQueryData<FormPreferences>(FORM_PREFERENCES_QUERY_KEY, next);
await AsyncStorage.setItem(FORM_PREFERENCES_STORAGE_KEY, JSON.stringify(next));
},
[queryClient]
);
const updateProviderPreferences = useCallback(
async (provider: AgentProvider, updates: Partial<ProviderPreferences>) => {
const prev =
queryClient.getQueryData<FormPreferences>(FORM_PREFERENCES_QUERY_KEY) ??
DEFAULT_FORM_PREFERENCES;
const next: FormPreferences = {
...prev,
providerPreferences: {
...prev.providerPreferences,
[provider]: {
...prev.providerPreferences?.[provider],
...updates,
},
},
};
queryClient.setQueryData<FormPreferences>(FORM_PREFERENCES_QUERY_KEY, next);
await AsyncStorage.setItem(FORM_PREFERENCES_STORAGE_KEY, JSON.stringify(next));
},
[queryClient]
);
return {
preferences,
isLoading: isPending,
getProviderPreferences,
updatePreferences,
updateProviderPreferences,
};
}