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

View File

@@ -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;

View File

@@ -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<AgentProvider, AgentProviderDefinition>;
agentDefinition?: AgentProviderDefinition;
@@ -66,8 +59,6 @@ type UseAgentFormStateResult = {
persistFormPreferences: () => Promise<void>;
};
const FORM_PREFERENCES_STORAGE_KEY = "@paseo:create-agent-preferences";
const providerDefinitions = AGENT_PROVIDER_DEFINITIONS;
const providerDefinitionMap = new Map<AgentProvider, AgentProviderDefinition>(
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<string | null>(
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<string, ReturnType<typeof setTimeout>>
@@ -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<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 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<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]);
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,

View File

@@ -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,
};
}