@assistant-ui/react-hook-form 0.12.34 → 0.12.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 AgentbaseAI Inc.
3
+ Copyright (c) 2026 AgentbaseAI Inc.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,21 +1,19 @@
1
1
  import { z } from "zod";
2
- //#region src/formTools.d.ts
3
2
  export declare const formTools: {
4
- set_form_field: {
5
- description: string;
6
- parameters: z.ZodObject<{
7
- name: z.ZodString;
8
- value: z.ZodString;
9
- }, z.core.$strip>;
10
- };
11
- submit_form: {
12
- description: string;
13
- parameters: z.ZodObject<{}, z.core.$strip>;
14
- };
15
- reset_form: {
16
- description: string;
17
- parameters: z.ZodObject<{}, z.core.$strip>;
18
- };
3
+ set_form_field: {
4
+ description: string;
5
+ parameters: z.ZodObject<{
6
+ name: z.ZodString;
7
+ value: z.ZodString;
8
+ }, z.core.$strip>;
9
+ };
10
+ submit_form: {
11
+ description: string;
12
+ parameters: z.ZodObject<{}, z.core.$strip>;
13
+ };
14
+ reset_form: {
15
+ description: string;
16
+ parameters: z.ZodObject<{}, z.core.$strip>;
17
+ };
19
18
  };
20
- //#endregion
21
19
  //# sourceMappingURL=formTools.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"formTools.d.ts","names":[],"sources":["../src/formTools.ts"],"mappings":";;qBAEa"}
1
+ {"version":3,"file":"formTools.d.ts","sourceRoot":"","sources":["../src/formTools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;CAkBrB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import { formTools } from "./formTools.js";
2
- import { UseAssistantFormProps, useAssistantForm } from "./useAssistantForm.js";
3
- export { type UseAssistantFormProps, formTools, useAssistantForm };
1
+ export { useAssistantForm, type UseAssistantFormProps, } from "./useAssistantForm.js";
2
+ export { formTools } from "./formTools.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,KAAK,qBAAqB,GAC3B,8BAA2B;AAC5B,OAAO,EAAE,SAAS,EAAE,uBAAoB"}
@@ -1,23 +1,21 @@
1
+ import { type ToolCallMessagePartComponent } from "@assistant-ui/core/react";
2
+ import { type FieldValues, type UseFormProps, type UseFormReturn } from "react-hook-form";
3
+ import type { z } from "zod";
1
4
  import { formTools } from "./formTools.js";
2
- import { z } from "zod";
3
- import { ToolCallMessagePartComponent } from "@assistant-ui/core/react";
4
- import { FieldValues, UseFormProps, UseFormReturn } from "react-hook-form";
5
- //#region src/useAssistantForm.d.ts
6
5
  export type UseAssistantFormProps<TFieldValues extends FieldValues, TContext, TTransformedValues> = UseFormProps<TFieldValues, TContext, TTransformedValues> & {
7
- assistant?: {
8
- tools?: {
9
- set_form_field?: {
10
- render?: ToolCallMessagePartComponent<z.infer<(typeof formTools.set_form_field)["parameters"]>, unknown> | undefined;
11
- } | undefined;
12
- submit_form?: {
13
- render?: ToolCallMessagePartComponent<z.infer<(typeof formTools.submit_form)["parameters"]>, unknown> | undefined;
14
- } | undefined;
15
- reset_form?: {
16
- render?: ToolCallMessagePartComponent<z.infer<(typeof formTools.reset_form)["parameters"]>, unknown> | undefined;
17
- } | undefined;
6
+ assistant?: {
7
+ tools?: {
8
+ set_form_field?: {
9
+ render?: ToolCallMessagePartComponent<z.infer<(typeof formTools.set_form_field)["parameters"]>, unknown> | undefined;
10
+ } | undefined;
11
+ submit_form?: {
12
+ render?: ToolCallMessagePartComponent<z.infer<(typeof formTools.submit_form)["parameters"]>, unknown> | undefined;
13
+ } | undefined;
14
+ reset_form?: {
15
+ render?: ToolCallMessagePartComponent<z.infer<(typeof formTools.reset_form)["parameters"]>, unknown> | undefined;
16
+ } | undefined;
17
+ } | undefined;
18
18
  } | undefined;
19
- } | undefined;
20
19
  };
21
20
  export declare const useAssistantForm: <TFieldValues extends FieldValues = FieldValues, TContext = any, TTransformedValues = TFieldValues>(props?: UseAssistantFormProps<TFieldValues, TContext, TTransformedValues>) => UseFormReturn<TFieldValues, TContext, TTransformedValues>;
22
- //#endregion
23
21
  //# sourceMappingURL=useAssistantForm.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useAssistantForm.d.ts","names":[],"sources":["../src/useAssistantForm.ts"],"mappings":";;;;;YA6BY,sBACV,qBAAqB,aACrB,UACA,sBACE,aAAa,cAAc,UAAU;EACvC;IAEM;MAEM;QAEM,SACI,6BACE,EAAE,cACQ,UAAU;;MAOhC;QAEM,SACI,6BACE,EAAE,cAAc,UAAU;;MAMtC;QAEM,SACI,6BACE,EAAE,cAAc,UAAU;;;;;qBAYvC,mBACX,qBAAqB,cAAc,aACnC,gBACA,qBAAqB,cAErB,QAAQ,sBAAsB,cAAc,UAAU,wBACrD,cAAc,cAAc,UAAU"}
1
+ {"version":3,"file":"useAssistantForm.d.ts","sourceRoot":"","sources":["../src/useAssistantForm.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,4BAA4B,EAAE,MAAM,0BAA0B,CAAC;AAG7E,OAAO,EAEL,KAAK,WAAW,EAGhB,KAAK,YAAY,EACjB,KAAK,aAAa,EAGnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,uBAAoB;AAYxC,MAAM,MAAM,qBAAqB,CAC/B,YAAY,SAAS,WAAW,EAChC,QAAQ,EACR,kBAAkB,IAChB,YAAY,CAAC,YAAY,EAAE,QAAQ,EAAE,kBAAkB,CAAC,GAAG;IAC7D,SAAS,CAAC,EACN;QACE,KAAK,CAAC,EACF;YACE,cAAc,CAAC,EACX;gBACE,MAAM,CAAC,EACH,4BAA4B,CAC1B,CAAC,CAAC,KAAK,CACL,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,CAAC,YAAY,CAAC,CAChD,EACD,OAAO,CACR,GACD,SAAS,CAAC;aACf,GACD,SAAS,CAAC;YACd,WAAW,CAAC,EACR;gBACE,MAAM,CAAC,EACH,4BAA4B,CAC1B,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,SAAS,CAAC,WAAW,CAAC,CAAC,YAAY,CAAC,CAAC,EACrD,OAAO,CACR,GACD,SAAS,CAAC;aACf,GACD,SAAS,CAAC;YACd,UAAU,CAAC,EACP;gBACE,MAAM,CAAC,EACH,4BAA4B,CAC1B,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,SAAS,CAAC,UAAU,CAAC,CAAC,YAAY,CAAC,CAAC,EACpD,OAAO,CACR,GACD,SAAS,CAAC;aACf,GACD,SAAS,CAAC;SACf,GACD,SAAS,CAAC;KACf,GACD,SAAS,CAAC;CACf,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAC3B,YAAY,SAAS,WAAW,GAAG,WAAW,EAC9C,QAAQ,GAAG,GAAG,EACd,kBAAkB,GAAG,YAAY,EAEjC,QAAQ,qBAAqB,CAAC,YAAY,EAAE,QAAQ,EAAE,kBAAkB,CAAC,KACxE,aAAa,CAAC,YAAY,EAAE,QAAQ,EAAE,kBAAkB,CA2R1D,CAAC"}
@@ -22,16 +22,28 @@ const useAssistantForm = (props) => {
22
22
  pendingAssistantSubmitRef.current = null;
23
23
  pending.reject(error);
24
24
  }, []);
25
+ useEffect(() => () => {
26
+ const pending = pendingAssistantSubmitRef.current;
27
+ if (!pending) return;
28
+ pending.unmounted = true;
29
+ pending.resolve(false);
30
+ }, []);
25
31
  const handleSubmit = useCallback((onValid, onInvalid) => {
26
32
  const submit = baseHandleSubmit((...args) => {
27
33
  const pending = pendingAssistantSubmitRef.current;
28
34
  const event = args[1]?.nativeEvent ?? args[1];
29
- if (pending && pending.event === event) pending.outcome = true;
35
+ if (pending && pending.event === event) {
36
+ pending.outcome = true;
37
+ if (pending.unmounted) return;
38
+ }
30
39
  return onValid(...args);
31
40
  }, (...args) => {
32
41
  const pending = pendingAssistantSubmitRef.current;
33
42
  const event = args[1]?.nativeEvent ?? args[1];
34
- if (pending && pending.event === event) pending.outcome = false;
43
+ if (pending && pending.event === event) {
44
+ pending.outcome = false;
45
+ if (pending.unmounted) return;
46
+ }
35
47
  return onInvalid?.(...args);
36
48
  });
37
49
  return (async (event) => {
@@ -109,6 +121,7 @@ const useAssistantForm = (props) => {
109
121
  dispatching: true,
110
122
  event: void 0,
111
123
  handlerInvoked: false,
124
+ unmounted: false,
112
125
  outcome: void 0,
113
126
  resolve: resolveSubmission,
114
127
  reject: rejectSubmission
@@ -135,7 +148,7 @@ const useAssistantForm = (props) => {
135
148
  if (await submissionResult) return { success: true };
136
149
  return {
137
150
  success: false,
138
- message: dispatched ? "The form contains invalid fields and was not submitted." : "The form did not accept the submission."
151
+ message: assistantSubmit.unmounted ? "The form is no longer available." : dispatched ? "The form contains invalid fields and was not submitted." : "The form did not accept the submission."
139
152
  };
140
153
  }
141
154
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"useAssistantForm.js","names":[],"sources":["../src/useAssistantForm.ts"],"sourcesContent":["\"use client\";\n\nimport { type ModelContext, tool } from \"@assistant-ui/core\";\nimport type {} from \"@assistant-ui/core/store\";\nimport { type ToolCallMessagePartComponent } from \"@assistant-ui/core/react\";\nimport { useAui } from \"@assistant-ui/store\";\nimport { useCallback, useEffect, useMemo, useRef } from \"react\";\nimport {\n type Field,\n type FieldValues,\n type Path,\n type PathValue,\n type UseFormProps,\n type UseFormReturn,\n get,\n useForm,\n} from \"react-hook-form\";\nimport type { z } from \"zod\";\nimport { formTools } from \"./formTools\";\n\ntype PendingAssistantSubmit = {\n dispatching: boolean;\n event: unknown;\n handlerInvoked: boolean;\n outcome: boolean | undefined;\n resolve: (outcome: boolean) => void;\n reject: (error: unknown) => void;\n};\n\nexport type UseAssistantFormProps<\n TFieldValues extends FieldValues,\n TContext,\n TTransformedValues,\n> = UseFormProps<TFieldValues, TContext, TTransformedValues> & {\n assistant?:\n | {\n tools?:\n | {\n set_form_field?:\n | {\n render?:\n | ToolCallMessagePartComponent<\n z.infer<\n (typeof formTools.set_form_field)[\"parameters\"]\n >,\n unknown\n >\n | undefined;\n }\n | undefined;\n submit_form?:\n | {\n render?:\n | ToolCallMessagePartComponent<\n z.infer<(typeof formTools.submit_form)[\"parameters\"]>,\n unknown\n >\n | undefined;\n }\n | undefined;\n reset_form?:\n | {\n render?:\n | ToolCallMessagePartComponent<\n z.infer<(typeof formTools.reset_form)[\"parameters\"]>,\n unknown\n >\n | undefined;\n }\n | undefined;\n }\n | undefined;\n }\n | undefined;\n};\n\nexport const useAssistantForm = <\n TFieldValues extends FieldValues = FieldValues,\n TContext = any,\n TTransformedValues = TFieldValues,\n>(\n props?: UseAssistantFormProps<TFieldValues, TContext, TTransformedValues>,\n): UseFormReturn<TFieldValues, TContext, TTransformedValues> => {\n const form = useForm<TFieldValues, TContext, TTransformedValues>(props);\n const {\n control,\n getValues,\n setValue,\n reset,\n handleSubmit: baseHandleSubmit,\n formState: { isSubmitting },\n } = form;\n\n const pendingAssistantSubmitRef = useRef<PendingAssistantSubmit | null>(null);\n const settleAssistantSubmit = useCallback((outcome: boolean) => {\n const pending = pendingAssistantSubmitRef.current;\n if (!pending) return;\n\n pendingAssistantSubmitRef.current = null;\n pending.resolve(outcome);\n }, []);\n const rejectAssistantSubmit = useCallback((error: unknown) => {\n const pending = pendingAssistantSubmitRef.current;\n if (!pending) return;\n\n pendingAssistantSubmitRef.current = null;\n pending.reject(error);\n }, []);\n\n const handleSubmit = useCallback<\n UseFormReturn<TFieldValues, TContext, TTransformedValues>[\"handleSubmit\"]\n >(\n (onValid, onInvalid) => {\n const submit = baseHandleSubmit(\n (...args) => {\n const pending = pendingAssistantSubmitRef.current;\n const event = args[1]?.nativeEvent ?? args[1];\n if (pending && pending.event === event) pending.outcome = true;\n return onValid(...args);\n },\n (...args) => {\n const pending = pendingAssistantSubmitRef.current;\n const event = args[1]?.nativeEvent ?? args[1];\n if (pending && pending.event === event) pending.outcome = false;\n return onInvalid?.(...args);\n },\n );\n\n return (async (event) => {\n const pending = pendingAssistantSubmitRef.current;\n const nativeEvent = event?.nativeEvent ?? event;\n const assistantSubmit =\n pending &&\n (pending.event === nativeEvent ||\n (pending.dispatching &&\n pending.event === undefined &&\n nativeEvent !== undefined))\n ? pending\n : null;\n if (assistantSubmit) {\n assistantSubmit.event = nativeEvent;\n assistantSubmit.handlerInvoked = true;\n }\n\n try {\n const result = await submit(event);\n if (\n assistantSubmit &&\n pendingAssistantSubmitRef.current === assistantSubmit\n ) {\n settleAssistantSubmit(assistantSubmit.outcome ?? true);\n }\n return result;\n } catch (error) {\n if (pendingAssistantSubmitRef.current === assistantSubmit) {\n rejectAssistantSubmit(error);\n }\n throw error;\n }\n }) as typeof submit;\n },\n [baseHandleSubmit, rejectAssistantSubmit, settleAssistantSubmit],\n );\n const assistantForm = useMemo<\n UseFormReturn<TFieldValues, TContext, TTransformedValues>\n >(\n () => ({\n ...form,\n handleSubmit,\n get formState() {\n return form.formState;\n },\n }),\n [form, handleSubmit],\n );\n\n const aui = useAui();\n useEffect(() => {\n const value: ModelContext = {\n tools: {\n set_form_field: tool({\n ...formTools.set_form_field,\n parameters: formTools.set_form_field.parameters,\n execute: async (args) => {\n setValue(\n args.name as Path<TFieldValues>,\n args.value as PathValue<TFieldValues, Path<TFieldValues>>,\n );\n\n return { success: true };\n },\n }),\n submit_form: tool({\n ...formTools.submit_form,\n execute: async () => {\n if (isSubmitting || pendingAssistantSubmitRef.current) {\n return {\n success: false,\n message: \"The form is already submitting.\",\n };\n }\n const { _names, _fields } = control;\n let formElement: HTMLFormElement | null = null;\n for (const name of _names.mount) {\n const field: Field | undefined = get(_fields, name);\n if (field?._f) {\n const fieldReference: unknown = Array.isArray(field._f.refs)\n ? field._f.refs[0]\n : field._f.ref;\n\n if (fieldReference instanceof HTMLElement) {\n if (\"form\" in fieldReference) {\n formElement =\n fieldReference.form instanceof HTMLFormElement\n ? fieldReference.form\n : null;\n } else {\n formElement = fieldReference.closest(\"form\");\n }\n if (formElement) break;\n }\n }\n }\n\n if (formElement) {\n if (!formElement.noValidate && !formElement.reportValidity()) {\n return {\n success: false,\n message:\n \"The form contains invalid fields and was not submitted.\",\n };\n }\n\n let resolveSubmission: (outcome: boolean) => void = () => {};\n let rejectSubmission: (error: unknown) => void = () => {};\n const submissionResult = new Promise<boolean>(\n (resolve, reject) => {\n resolveSubmission = resolve;\n rejectSubmission = reject;\n },\n );\n const assistantSubmit: PendingAssistantSubmit = {\n dispatching: true,\n event: undefined,\n handlerInvoked: false,\n outcome: undefined,\n resolve: resolveSubmission,\n reject: rejectSubmission,\n };\n pendingAssistantSubmitRef.current = assistantSubmit;\n const onSubmit = (event: SubmitEvent) => {\n if (\n pendingAssistantSubmitRef.current === assistantSubmit &&\n assistantSubmit.event === undefined\n ) {\n assistantSubmit.event = event;\n }\n queueMicrotask(() => {\n if (\n pendingAssistantSubmitRef.current === assistantSubmit &&\n !assistantSubmit.handlerInvoked\n ) {\n settleAssistantSubmit(true);\n }\n });\n };\n\n formElement.addEventListener(\"submit\", onSubmit, { once: true });\n try {\n formElement.requestSubmit();\n } catch (error) {\n settleAssistantSubmit(false);\n throw error;\n } finally {\n assistantSubmit.dispatching = false;\n formElement.removeEventListener(\"submit\", onSubmit);\n }\n\n const dispatched = assistantSubmit.event !== undefined;\n if (!dispatched) {\n settleAssistantSubmit(false);\n }\n\n if (await submissionResult) return { success: true };\n return {\n success: false,\n message: dispatched\n ? \"The form contains invalid fields and was not submitted.\"\n : \"The form did not accept the submission.\",\n };\n }\n\n return {\n success: false,\n message:\n \"Unable retrieve the form element. This is a coding error.\",\n };\n },\n }),\n reset_form: tool({\n ...formTools.reset_form,\n execute: async () => {\n reset();\n return { success: true };\n },\n }),\n },\n };\n\n return aui.modelContext.register({\n getModelContext: () => ({\n ...value,\n system: `Form State:\\n${JSON.stringify(getValues())}`,\n }),\n });\n }, [\n control,\n setValue,\n getValues,\n aui,\n reset,\n isSubmitting,\n rejectAssistantSubmit,\n settleAssistantSubmit,\n ]);\n\n const renderFormFieldTool = props?.assistant?.tools?.set_form_field?.render;\n useEffect(() => {\n if (!renderFormFieldTool) return undefined;\n return aui.tools.setToolUI(\"set_form_field\", renderFormFieldTool);\n }, [aui, renderFormFieldTool]);\n\n const renderSubmitFormTool = props?.assistant?.tools?.submit_form?.render;\n useEffect(() => {\n if (!renderSubmitFormTool) return undefined;\n return aui.tools.setToolUI(\"submit_form\", renderSubmitFormTool);\n }, [aui, renderSubmitFormTool]);\n\n const renderResetFormTool = props?.assistant?.tools?.reset_form?.render;\n useEffect(() => {\n if (!renderResetFormTool) return undefined;\n return aui.tools.setToolUI(\"reset_form\", renderResetFormTool);\n }, [aui, renderResetFormTool]);\n\n return assistantForm;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useAssistantForm.js","names":[],"sources":["../src/useAssistantForm.ts"],"sourcesContent":["\"use client\";\n\nimport { type ModelContext, tool } from \"@assistant-ui/core\";\nimport type {} from \"@assistant-ui/core/store\";\nimport { type ToolCallMessagePartComponent } from \"@assistant-ui/core/react\";\nimport { useAui } from \"@assistant-ui/store\";\nimport { useCallback, useEffect, useMemo, useRef } from \"react\";\nimport {\n type Field,\n type FieldValues,\n type Path,\n type PathValue,\n type UseFormProps,\n type UseFormReturn,\n get,\n useForm,\n} from \"react-hook-form\";\nimport type { z } from \"zod\";\nimport { formTools } from \"./formTools\";\n\ntype PendingAssistantSubmit = {\n dispatching: boolean;\n event: unknown;\n handlerInvoked: boolean;\n unmounted: boolean;\n outcome: boolean | undefined;\n resolve: (outcome: boolean) => void;\n reject: (error: unknown) => void;\n};\n\nexport type UseAssistantFormProps<\n TFieldValues extends FieldValues,\n TContext,\n TTransformedValues,\n> = UseFormProps<TFieldValues, TContext, TTransformedValues> & {\n assistant?:\n | {\n tools?:\n | {\n set_form_field?:\n | {\n render?:\n | ToolCallMessagePartComponent<\n z.infer<\n (typeof formTools.set_form_field)[\"parameters\"]\n >,\n unknown\n >\n | undefined;\n }\n | undefined;\n submit_form?:\n | {\n render?:\n | ToolCallMessagePartComponent<\n z.infer<(typeof formTools.submit_form)[\"parameters\"]>,\n unknown\n >\n | undefined;\n }\n | undefined;\n reset_form?:\n | {\n render?:\n | ToolCallMessagePartComponent<\n z.infer<(typeof formTools.reset_form)[\"parameters\"]>,\n unknown\n >\n | undefined;\n }\n | undefined;\n }\n | undefined;\n }\n | undefined;\n};\n\nexport const useAssistantForm = <\n TFieldValues extends FieldValues = FieldValues,\n TContext = any,\n TTransformedValues = TFieldValues,\n>(\n props?: UseAssistantFormProps<TFieldValues, TContext, TTransformedValues>,\n): UseFormReturn<TFieldValues, TContext, TTransformedValues> => {\n const form = useForm<TFieldValues, TContext, TTransformedValues>(props);\n const {\n control,\n getValues,\n setValue,\n reset,\n handleSubmit: baseHandleSubmit,\n formState: { isSubmitting },\n } = form;\n\n const pendingAssistantSubmitRef = useRef<PendingAssistantSubmit | null>(null);\n const settleAssistantSubmit = useCallback((outcome: boolean) => {\n const pending = pendingAssistantSubmitRef.current;\n if (!pending) return;\n\n pendingAssistantSubmitRef.current = null;\n pending.resolve(outcome);\n }, []);\n const rejectAssistantSubmit = useCallback((error: unknown) => {\n const pending = pendingAssistantSubmitRef.current;\n if (!pending) return;\n\n pendingAssistantSubmitRef.current = null;\n pending.reject(error);\n }, []);\n\n useEffect(\n () => () => {\n const pending = pendingAssistantSubmitRef.current;\n if (!pending) return;\n\n pending.unmounted = true;\n pending.resolve(false);\n },\n [],\n );\n\n const handleSubmit = useCallback<\n UseFormReturn<TFieldValues, TContext, TTransformedValues>[\"handleSubmit\"]\n >(\n (onValid, onInvalid) => {\n const submit = baseHandleSubmit(\n (...args) => {\n const pending = pendingAssistantSubmitRef.current;\n const event = args[1]?.nativeEvent ?? args[1];\n if (pending && pending.event === event) {\n pending.outcome = true;\n if (pending.unmounted) return;\n }\n return onValid(...args);\n },\n (...args) => {\n const pending = pendingAssistantSubmitRef.current;\n const event = args[1]?.nativeEvent ?? args[1];\n if (pending && pending.event === event) {\n pending.outcome = false;\n if (pending.unmounted) return;\n }\n return onInvalid?.(...args);\n },\n );\n\n return (async (event) => {\n const pending = pendingAssistantSubmitRef.current;\n const nativeEvent = event?.nativeEvent ?? event;\n const assistantSubmit =\n pending &&\n (pending.event === nativeEvent ||\n (pending.dispatching &&\n pending.event === undefined &&\n nativeEvent !== undefined))\n ? pending\n : null;\n if (assistantSubmit) {\n assistantSubmit.event = nativeEvent;\n assistantSubmit.handlerInvoked = true;\n }\n\n try {\n const result = await submit(event);\n if (\n assistantSubmit &&\n pendingAssistantSubmitRef.current === assistantSubmit\n ) {\n settleAssistantSubmit(assistantSubmit.outcome ?? true);\n }\n return result;\n } catch (error) {\n if (pendingAssistantSubmitRef.current === assistantSubmit) {\n rejectAssistantSubmit(error);\n }\n throw error;\n }\n }) as typeof submit;\n },\n [baseHandleSubmit, rejectAssistantSubmit, settleAssistantSubmit],\n );\n const assistantForm = useMemo<\n UseFormReturn<TFieldValues, TContext, TTransformedValues>\n >(\n () => ({\n ...form,\n handleSubmit,\n get formState() {\n return form.formState;\n },\n }),\n [form, handleSubmit],\n );\n\n const aui = useAui();\n useEffect(() => {\n const value: ModelContext = {\n tools: {\n set_form_field: tool({\n ...formTools.set_form_field,\n parameters: formTools.set_form_field.parameters,\n execute: async (args) => {\n setValue(\n args.name as Path<TFieldValues>,\n args.value as PathValue<TFieldValues, Path<TFieldValues>>,\n );\n\n return { success: true };\n },\n }),\n submit_form: tool({\n ...formTools.submit_form,\n execute: async () => {\n if (isSubmitting || pendingAssistantSubmitRef.current) {\n return {\n success: false,\n message: \"The form is already submitting.\",\n };\n }\n const { _names, _fields } = control;\n let formElement: HTMLFormElement | null = null;\n for (const name of _names.mount) {\n const field: Field | undefined = get(_fields, name);\n if (field?._f) {\n const fieldReference: unknown = Array.isArray(field._f.refs)\n ? field._f.refs[0]\n : field._f.ref;\n\n if (fieldReference instanceof HTMLElement) {\n if (\"form\" in fieldReference) {\n formElement =\n fieldReference.form instanceof HTMLFormElement\n ? fieldReference.form\n : null;\n } else {\n formElement = fieldReference.closest(\"form\");\n }\n if (formElement) break;\n }\n }\n }\n\n if (formElement) {\n if (!formElement.noValidate && !formElement.reportValidity()) {\n return {\n success: false,\n message:\n \"The form contains invalid fields and was not submitted.\",\n };\n }\n\n let resolveSubmission: (outcome: boolean) => void = () => {};\n let rejectSubmission: (error: unknown) => void = () => {};\n const submissionResult = new Promise<boolean>(\n (resolve, reject) => {\n resolveSubmission = resolve;\n rejectSubmission = reject;\n },\n );\n const assistantSubmit: PendingAssistantSubmit = {\n dispatching: true,\n event: undefined,\n handlerInvoked: false,\n unmounted: false,\n outcome: undefined,\n resolve: resolveSubmission,\n reject: rejectSubmission,\n };\n pendingAssistantSubmitRef.current = assistantSubmit;\n const onSubmit = (event: SubmitEvent) => {\n if (\n pendingAssistantSubmitRef.current === assistantSubmit &&\n assistantSubmit.event === undefined\n ) {\n assistantSubmit.event = event;\n }\n queueMicrotask(() => {\n if (\n pendingAssistantSubmitRef.current === assistantSubmit &&\n !assistantSubmit.handlerInvoked\n ) {\n settleAssistantSubmit(true);\n }\n });\n };\n\n formElement.addEventListener(\"submit\", onSubmit, { once: true });\n try {\n formElement.requestSubmit();\n } catch (error) {\n settleAssistantSubmit(false);\n throw error;\n } finally {\n assistantSubmit.dispatching = false;\n formElement.removeEventListener(\"submit\", onSubmit);\n }\n\n const dispatched = assistantSubmit.event !== undefined;\n if (!dispatched) {\n settleAssistantSubmit(false);\n }\n\n if (await submissionResult) return { success: true };\n return {\n success: false,\n message: assistantSubmit.unmounted\n ? \"The form is no longer available.\"\n : dispatched\n ? \"The form contains invalid fields and was not submitted.\"\n : \"The form did not accept the submission.\",\n };\n }\n\n return {\n success: false,\n message:\n \"Unable retrieve the form element. This is a coding error.\",\n };\n },\n }),\n reset_form: tool({\n ...formTools.reset_form,\n execute: async () => {\n reset();\n return { success: true };\n },\n }),\n },\n };\n\n return aui.modelContext.register({\n getModelContext: () => ({\n ...value,\n system: `Form State:\\n${JSON.stringify(getValues())}`,\n }),\n });\n }, [\n control,\n setValue,\n getValues,\n aui,\n reset,\n isSubmitting,\n rejectAssistantSubmit,\n settleAssistantSubmit,\n ]);\n\n const renderFormFieldTool = props?.assistant?.tools?.set_form_field?.render;\n useEffect(() => {\n if (!renderFormFieldTool) return undefined;\n return aui.tools.setToolUI(\"set_form_field\", renderFormFieldTool);\n }, [aui, renderFormFieldTool]);\n\n const renderSubmitFormTool = props?.assistant?.tools?.submit_form?.render;\n useEffect(() => {\n if (!renderSubmitFormTool) return undefined;\n return aui.tools.setToolUI(\"submit_form\", renderSubmitFormTool);\n }, [aui, renderSubmitFormTool]);\n\n const renderResetFormTool = props?.assistant?.tools?.reset_form?.render;\n useEffect(() => {\n if (!renderResetFormTool) return undefined;\n return aui.tools.setToolUI(\"reset_form\", renderResetFormTool);\n }, [aui, renderResetFormTool]);\n\n return assistantForm;\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-hook-form",
3
- "version": "0.12.34",
3
+ "version": "0.12.36",
4
4
  "description": "React Hook Form integration for assistant-ui",
5
5
  "keywords": [
6
6
  "react-hook-form",
@@ -29,8 +29,8 @@
29
29
  ],
30
30
  "sideEffects": false,
31
31
  "dependencies": {
32
- "@assistant-ui/core": "^0.3.20",
33
- "@assistant-ui/store": "^0.3.14",
32
+ "@assistant-ui/core": "^0.3.23",
33
+ "@assistant-ui/store": "^0.3.17",
34
34
  "zod": "^4.6.5"
35
35
  },
36
36
  "peerDependencies": {
@@ -44,14 +44,15 @@
44
44
  }
45
45
  },
46
46
  "devDependencies": {
47
- "@assistant-ui/x-buildutils": "0.0.28",
47
+ "@assistant-ui/x-buildutils": "0.0.30",
48
+ "@assistant-ui/x-react18": "0.0.0",
48
49
  "@testing-library/react": "^16.3.3",
49
50
  "@types/react": "^19.3.0",
50
51
  "@types/react-dom": "^19.3.0",
51
52
  "jsdom": "^30.0.1",
52
53
  "react": "^19.3.0",
53
- "react-hook-form": "^7.88.0",
54
- "vitest": "^5.0.1"
54
+ "react-hook-form": "^7.89.0",
55
+ "vitest": "^5.0.3"
55
56
  },
56
57
  "publishConfig": {
57
58
  "access": "public",
@@ -70,6 +71,8 @@
70
71
  "build": "aui-build",
71
72
  "test": "vitest run",
72
73
  "typecheck": "tsc --noEmit",
74
+ "test:peer-react18": "vitest run --config vitest.peer-react18.config.ts",
75
+ "test:types:peer-react18": "aui-react18-types",
73
76
  "test:watch": "vitest"
74
77
  }
75
78
  }
@@ -392,6 +392,43 @@ describe("useAssistantForm", () => {
392
392
  });
393
393
  });
394
394
 
395
+ it("settles a pending assistant submission when the form unmounts", async () => {
396
+ type FormValues = { name: string };
397
+ let resolveValidation!: (result: ResolverResult<FormValues>) => void;
398
+ const resolver: Resolver<FormValues> = vi.fn(
399
+ () =>
400
+ new Promise<ResolverResult<FormValues>>((resolve) => {
401
+ resolveValidation = resolve;
402
+ }),
403
+ );
404
+ const onValid = vi.fn();
405
+ const Form = () => {
406
+ const form = useAssistantForm<FormValues>({ resolver });
407
+ return (
408
+ <form onSubmit={form.handleSubmit(onValid)}>
409
+ <input {...form.register("name")} />
410
+ </form>
411
+ );
412
+ };
413
+ const { unmount } = render(<Form />);
414
+
415
+ const assistantSubmit = executeSubmitForm();
416
+ await waitFor(() => expect(resolver).toHaveBeenCalledOnce());
417
+
418
+ unmount();
419
+
420
+ await expect(assistantSubmit).resolves.toEqual({
421
+ success: false,
422
+ message: "The form is no longer available.",
423
+ });
424
+
425
+ await act(async () => {
426
+ resolveValidation({ values: { name: "Ada" }, errors: {} });
427
+ await new Promise((resolve) => setTimeout(resolve, 0));
428
+ });
429
+ expect(onValid).not.toHaveBeenCalled();
430
+ });
431
+
395
432
  it("reports when requestSubmit does not dispatch a submit event", async () => {
396
433
  const requestSubmit = vi
397
434
  .spyOn(HTMLFormElement.prototype, "requestSubmit")
@@ -22,6 +22,7 @@ type PendingAssistantSubmit = {
22
22
  dispatching: boolean;
23
23
  event: unknown;
24
24
  handlerInvoked: boolean;
25
+ unmounted: boolean;
25
26
  outcome: boolean | undefined;
26
27
  resolve: (outcome: boolean) => void;
27
28
  reject: (error: unknown) => void;
@@ -107,6 +108,17 @@ export const useAssistantForm = <
107
108
  pending.reject(error);
108
109
  }, []);
109
110
 
111
+ useEffect(
112
+ () => () => {
113
+ const pending = pendingAssistantSubmitRef.current;
114
+ if (!pending) return;
115
+
116
+ pending.unmounted = true;
117
+ pending.resolve(false);
118
+ },
119
+ [],
120
+ );
121
+
110
122
  const handleSubmit = useCallback<
111
123
  UseFormReturn<TFieldValues, TContext, TTransformedValues>["handleSubmit"]
112
124
  >(
@@ -115,13 +127,19 @@ export const useAssistantForm = <
115
127
  (...args) => {
116
128
  const pending = pendingAssistantSubmitRef.current;
117
129
  const event = args[1]?.nativeEvent ?? args[1];
118
- if (pending && pending.event === event) pending.outcome = true;
130
+ if (pending && pending.event === event) {
131
+ pending.outcome = true;
132
+ if (pending.unmounted) return;
133
+ }
119
134
  return onValid(...args);
120
135
  },
121
136
  (...args) => {
122
137
  const pending = pendingAssistantSubmitRef.current;
123
138
  const event = args[1]?.nativeEvent ?? args[1];
124
- if (pending && pending.event === event) pending.outcome = false;
139
+ if (pending && pending.event === event) {
140
+ pending.outcome = false;
141
+ if (pending.unmounted) return;
142
+ }
125
143
  return onInvalid?.(...args);
126
144
  },
127
145
  );
@@ -243,6 +261,7 @@ export const useAssistantForm = <
243
261
  dispatching: true,
244
262
  event: undefined,
245
263
  handlerInvoked: false,
264
+ unmounted: false,
246
265
  outcome: undefined,
247
266
  resolve: resolveSubmission,
248
267
  reject: rejectSubmission,
@@ -284,9 +303,11 @@ export const useAssistantForm = <
284
303
  if (await submissionResult) return { success: true };
285
304
  return {
286
305
  success: false,
287
- message: dispatched
288
- ? "The form contains invalid fields and was not submitted."
289
- : "The form did not accept the submission.",
306
+ message: assistantSubmit.unmounted
307
+ ? "The form is no longer available."
308
+ : dispatched
309
+ ? "The form contains invalid fields and was not submitted."
310
+ : "The form did not accept the submission.",
290
311
  };
291
312
  }
292
313