name: project-form-patterns
description: Form implementation guidance for the aghub desktop app. Use when building or refactoring forms in crates/desktop/src, especially HeroUI v3 + React forms, RHF integration, validation, custom editors, and error presentation. Triggers: MCP forms, settings dialogs, create/edit panels, TextField, FieldError, Select, react-hook-form, key-value editors, validation behavior.
AGHub Form Patterns
Follow these rules when building forms in this project.
Source of Truth
- Check the official docs first, not memory and not bundled/local docs, when form behavior is in question.
- For HeroUI, prefer the official site:
https://v3.heroui.com/docs/react/components/form
https://v3.heroui.com/docs/react/components/text-field
https://v3.heroui.com/docs/react/components/field-error
https://v3.heroui.com/docs/react/components/select
- Use local skill docs only as a convenience after confirming the official API.
Default Stack
- Prefer
react-hook-form for non-trivial forms.
- Use
Controller for HeroUI Select and any custom controlled widgets.
- Keep one form state. Do not create a second derived validation state unless there is a concrete need the form library cannot express.
Validation Behavior
- When RHF controls validation, set HeroUI form fields to
validationBehavior="aria".
- Do not rely on HeroUI/native validation defaults together with RHF. Native validation can steal focus and submission flow while bypassing the error UI you expect from RHF.
- Let RHF own validation rules and submission blocking.
Error Rendering
- For HeroUI text fields, use:
isInvalid={Boolean(fieldState.error)}
- Conditionally render
<FieldError>{fieldState.error.message}</FieldError> inside the same TextField.
- Do not invent unsupported props. In particular, do not assume
TextField supports an errorMessage prop.
- Keep the official anatomy:
<TextField isInvalid={Boolean(fieldState.error)} validationBehavior="aria">
<Label>Name</Label>
<Input {...inputProps} />
{fieldState.error && <FieldError>{fieldState.error.message}</FieldError>}
</TextField>
- For non-form or custom composite controls, prefer HeroUI
ErrorMessage instead of hand-rolled error text.
- Use
ErrorMessage for collection-style or custom editors that are not true form fields, including key/value editors, tag selectors, and similar composite controls.
Custom Editors
- Custom widgets like
AgentSelector, EnvEditor, HttpHeaderEditor, and KeyPairEditor should still be registered in RHF through Controller.
- For custom editors that manage arrays or compound values, compute validation from the current field value and surface one aggregated error below the editor when possible.
- For that aggregated error, prefer HeroUI
ErrorMessage.
- Do not inject per-row error UI into tight horizontal layouts unless the design explicitly calls for it.
Key/Value Editors
- Keep row layout simple:
- two inputs
- one delete button
- no extra wrappers that change flex behavior unless necessary
- Prefer aggregate error text below the whole editor over inline row errors. This avoids breaking spacing and alignment.
- Implement that aggregate error with HeroUI
ErrorMessage, not a custom <p> block.
- If you must show row-level issues, redesign the layout first; do not bolt error blocks into a row that was designed as a single-line control.
Practical Rules
- Prefer
onPress for HeroUI buttons.
- Use
type="button" for non-submit buttons inside forms.
- Preserve existing visual patterns in this repo; do not restyle forms while adding validation.
- After form changes, run
bun run build in crates/desktop when possible and separate unrelated existing build failures from the changes you made.
Anti-Patterns
- Do not mix RHF validation with a parallel
validationErrors state for the same fields.
- Do not depend on HeroUI default native validation when you expect RHF errors to drive the UI.
- Do not push validation messages into each key/value row unless you intentionally redesign that editor.
- Do not trust remembered HeroUI APIs for forms without checking the official site first.