mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
feat(app): extract form preferences into dedicated hook with per-provider storage
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
103
packages/app/src/hooks/use-form-preferences.ts
Normal file
103
packages/app/src/hooks/use-form-preferences.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user