diff --git a/skyvern-frontend/src/hooks/useLocalStorageFormDefault.ts b/skyvern-frontend/src/hooks/useLocalStorageFormDefault.ts new file mode 100644 index 00000000..131b35f5 --- /dev/null +++ b/skyvern-frontend/src/hooks/useLocalStorageFormDefault.ts @@ -0,0 +1,20 @@ +/** + * Returns a value from localStorage for the given key, or a fallback if not present. + * Use this hook to initialize form default values from localStorage in a type-safe way. + * + * @param storageKey - The localStorage key to read + * @param fallback - The fallback value if localStorage is empty or unavailable + * @returns The value from localStorage (if present), otherwise the fallback + */ +import { useMemo } from "react"; + +export function useLocalStorageFormDefault( + storageKey: string, + fallback: string | null | undefined, +): string | null | undefined { + return useMemo(() => { + if (typeof window === "undefined") return fallback ?? null; + const value = localStorage.getItem(storageKey); + return value !== null ? value : fallback ?? null; + }, [storageKey, fallback]); +} diff --git a/skyvern-frontend/src/hooks/useSyncFormFieldToStorage.ts b/skyvern-frontend/src/hooks/useSyncFormFieldToStorage.ts new file mode 100644 index 00000000..7534447a --- /dev/null +++ b/skyvern-frontend/src/hooks/useSyncFormFieldToStorage.ts @@ -0,0 +1,23 @@ +import { useEffect } from "react"; +import { UseFormReturn, FieldValues, WatchObserver } from "react-hook-form"; + +/** + * Syncs a form field value to localStorage whenever it changes. + * @param form - A react-hook-form object with a .watch method + * @param fieldName - The name of the field to watch + * @param storageKey - The key to write to in localStorage + */ +export function useSyncFormFieldToStorage( + form: UseFormReturn, + fieldName: keyof T & string, + storageKey: string, +) { + useEffect(() => { + const subscription = form.watch(((value, { name }) => { + if (name === fieldName && typeof value[fieldName] === "string") { + localStorage.setItem(storageKey, value[fieldName] as string); + } + }) as WatchObserver); + return () => subscription.unsubscribe(); + }, [form, fieldName, storageKey]); +} diff --git a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx index 5d1a6748..da36beb4 100644 --- a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx +++ b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx @@ -14,6 +14,8 @@ import { Input } from "@/components/ui/input"; import { toast } from "@/components/ui/use-toast"; import { useApiCredential } from "@/hooks/useApiCredential"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; +import { useSyncFormFieldToStorage } from "@/hooks/useSyncFormFieldToStorage"; +import { useLocalStorageFormDefault } from "@/hooks/useLocalStorageFormDefault"; import { apiBaseUrl } from "@/util/env"; import { type ApiCommandOptions } from "@/util/apiCommands"; import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons"; @@ -70,22 +72,27 @@ type RunWorkflowRequestBody = { data: Record; // workflow parameters and values proxy_location: ProxyLocation | null; webhook_callback_url?: string | null; + browser_session_id: string | null; }; function getRunWorkflowRequestBody( values: RunWorkflowFormType, workflowParameters: Array, ): RunWorkflowRequestBody { - const { webhookCallbackUrl, proxyLocation, ...parameters } = values; + const { webhookCallbackUrl, proxyLocation, browserSessionId, ...parameters } = + values; const parsedParameters = parseValuesForWorkflowRun( parameters, workflowParameters, ); + const bsi = browserSessionId?.trim() === "" ? null : browserSessionId; + const body: RunWorkflowRequestBody = { data: parsedParameters, proxy_location: proxyLocation, + browser_session_id: bsi, }; if (webhookCallbackUrl) { @@ -98,6 +105,7 @@ function getRunWorkflowRequestBody( type RunWorkflowFormType = Record & { webhookCallbackUrl: string; proxyLocation: ProxyLocation; + browserSessionId: string | null; }; function RunWorkflowForm({ @@ -109,15 +117,26 @@ function RunWorkflowForm({ const credentialGetter = useCredentialGetter(); const navigate = useNavigate(); const queryClient = useQueryClient(); + const browserSessionIdDefault = useLocalStorageFormDefault( + "skyvern.browserSessionId", + (initialValues.browserSessionId as string | undefined) ?? null, + ); const form = useForm({ defaultValues: { ...initialValues, webhookCallbackUrl: initialSettings.webhookCallbackUrl, proxyLocation: initialSettings.proxyLocation, + browserSessionId: browserSessionIdDefault, }, }); const apiCredential = useApiCredential(); + useSyncFormFieldToStorage( + form, + "browserSessionId", + "skyvern.browserSessionId", + ); + const runWorkflowMutation = useMutation({ mutationFn: async (values: RunWorkflowFormType) => { const client = await getClient(credentialGetter); @@ -154,7 +173,13 @@ function RunWorkflowForm({ }); function onSubmit(values: RunWorkflowFormType) { - const { webhookCallbackUrl, proxyLocation, ...parameters } = values; + const { + webhookCallbackUrl, + proxyLocation, + browserSessionId, + ...parameters + } = values; + const parsedParameters = parseValuesForWorkflowRun( parameters, workflowParameters, @@ -163,6 +188,7 @@ function RunWorkflowForm({ ...parsedParameters, webhookCallbackUrl, proxyLocation, + browserSessionId, }); } @@ -330,8 +356,43 @@ function RunWorkflowForm({ ); }} /> + { + return ( + +
+ +
+
+ Browser Session ID +
+

+ Use a persistent browser session to maintain state and + enable browser interaction. +

+
+
+
+ + + + +
+
+
+ ); + }} + /> -
{