feat(apollo-wind): guardrails component family (validator form + builder screen) - #1107
feat(apollo-wind): guardrails component family (validator form + builder screen)#1107apetraru-uipath wants to merge 4 commits into
Conversation
Shared configuration form for OOTB guardrail validators, extracted from Flow Workbench's canvas-local implementation so Flow and Agents render the same component. Covers all seven parameter types (number, text, boolean, enum, enum-list, text-list, map-enum incl. keySource-derived rows), a host-owned errors map with per-parameter clearing, a renderParameter override slot for product-specific editors (e.g. a judge-model picker), save-time companion utils (seedGuardrailParameters, syncMapEnumParameters, dropEmptyOptionalParameters, getRequiredEmptyParameterIds), and component-owned locale catalogs for 14 languages harvested from Flow's existing translations. Also wires the custom/ shelf into the public API (components/custom exports); only the guardrails family is exported, the pre-existing prototype modules stay private. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Apollo Coded App preview deployments are ready.
|
Dependency License Review
License distribution
Excluded packages
|
📦 Dev Packages
|
There was a problem hiding this comment.
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Introduces the first shared “Guardrails” component family in apollo-wind by adding GuardrailValidatorForm (a fully controlled, host-validated parameter editor) along with supporting types, i18n catalog loading, and save-time utilities. This also begins exposing components/custom as a public API surface for the package.
Changes:
- Added
GuardrailValidatorFormwith editor support for 7 parameter types plus an override slot (renderParameter) for product-specific editors. - Added guardrails utilities (
seedGuardrailParameters,syncMapEnumParameters,dropEmptyOptionalParameters,getRequiredEmptyParameterIds) and a lightweight locale-catalog loader with 14 locale modules. - Updated barrels/exports (
src/index.ts,src/components/*, andpackage.jsonexports) and adjusted.gitignoreto include handwritten locale TS sources.
File summaries
| File | Description |
|---|---|
| packages/apollo-wind/src/index.ts | Exposes the guardrails component family + utilities/types from the package root entrypoint. |
| packages/apollo-wind/src/components/index.ts | Adds custom/ shelf to the components barrel. |
| packages/apollo-wind/src/components/custom/index.ts | Publicly exports only the reviewed guardrails family from custom/. |
| packages/apollo-wind/src/components/custom/guardrails/index.ts | Guardrails family barrel (component, types, i18n, utilities). |
| packages/apollo-wind/src/components/custom/guardrails/types.ts | Defines the parameter/definition unions and the GuardrailValidatorFormProps contract. |
| packages/apollo-wind/src/components/custom/guardrails/utils.ts | Adds seeding/cleanup/sync helpers and the required-empty predicate for hosts. |
| packages/apollo-wind/src/components/custom/guardrails/utils.test.ts | Unit coverage for guardrails utilities (seeding, syncing, pruning, required-empty predicate). |
| packages/apollo-wind/src/components/custom/guardrails/i18n.ts | Defines chrome-string labels, default English labels, interpolation and merge logic. |
| packages/apollo-wind/src/components/custom/guardrails/load-messages.ts | Adds supported-locale list, locale normalization, and lazy catalog loading. |
| packages/apollo-wind/src/components/custom/guardrails/load-messages.test.ts | Tests locale resolution, catalog loading/fallback behavior, and label merging. |
| packages/apollo-wind/src/components/custom/guardrails/guardrail-validator-form.tsx | Implements the main controlled form renderer and async label resolution hook. |
| packages/apollo-wind/src/components/custom/guardrails/guardrail-validator-form.test.tsx | Comprehensive RTL + jest-axe coverage across all parameter types, overrides, i18n, a11y. |
| packages/apollo-wind/src/components/custom/guardrails/guardrail-validator-form.stories.tsx | Storybook coverage for interactive scenarios, errors, override slot, and localization. |
| packages/apollo-wind/src/components/custom/guardrails/components/parameter-label.tsx | Shared label + tooltip + inline error rendering primitives for parameter fields. |
| packages/apollo-wind/src/components/custom/guardrails/components/number-parameter-field.tsx | Number editor field implementation. |
| packages/apollo-wind/src/components/custom/guardrails/components/text-parameter-field.tsx | Multiline text editor field implementation. |
| packages/apollo-wind/src/components/custom/guardrails/components/boolean-parameter-field.tsx | Boolean editor field implementation. |
| packages/apollo-wind/src/components/custom/guardrails/components/enum-parameter-field.tsx | Enum select editor (including “synthetic option” behavior for stale stored values). |
| packages/apollo-wind/src/components/custom/guardrails/components/enum-list-parameter-field.tsx | Enum-list chips editor with popover fallback for long option lists. |
| packages/apollo-wind/src/components/custom/guardrails/components/map-enum-parameter-field.tsx | Map-enum editor rendering rows derived from a sibling enum-list parameter. |
| packages/apollo-wind/src/components/custom/guardrails/components/text-list-parameter-field.tsx | Text-list repeated-row editor with add/remove and stable-row key strategy. |
| packages/apollo-wind/src/components/custom/guardrails/README.md | Local package documentation for contract, utilities, localization, and shadow-DOM consumption notes. |
| packages/apollo-wind/src/components/custom/guardrails/locales/en.ts | English chrome-string catalog module. |
| packages/apollo-wind/src/components/custom/guardrails/locales/de.ts | German chrome-string catalog module. |
| packages/apollo-wind/src/components/custom/guardrails/locales/es.ts | Spanish chrome-string catalog module. |
| packages/apollo-wind/src/components/custom/guardrails/locales/es-MX.ts | Spanish (Mexico) chrome-string catalog module. |
| packages/apollo-wind/src/components/custom/guardrails/locales/fr.ts | French chrome-string catalog module. |
| packages/apollo-wind/src/components/custom/guardrails/locales/ja.ts | Japanese chrome-string catalog module. |
| packages/apollo-wind/src/components/custom/guardrails/locales/ko.ts | Korean chrome-string catalog module. |
| packages/apollo-wind/src/components/custom/guardrails/locales/pt.ts | Portuguese chrome-string catalog module. |
| packages/apollo-wind/src/components/custom/guardrails/locales/pt-BR.ts | Portuguese (Brazil) chrome-string catalog module. |
| packages/apollo-wind/src/components/custom/guardrails/locales/ro.ts | Romanian chrome-string catalog module. |
| packages/apollo-wind/src/components/custom/guardrails/locales/ru.ts | Russian chrome-string catalog module (currently empty; relies on per-key English fallback). |
| packages/apollo-wind/src/components/custom/guardrails/locales/tr.ts | Turkish chrome-string catalog module. |
| packages/apollo-wind/src/components/custom/guardrails/locales/zh-CN.ts | Chinese (Simplified) chrome-string catalog module. |
| packages/apollo-wind/src/components/custom/guardrails/locales/zh-TW.ts | Chinese (Traditional) chrome-string catalog module. |
| packages/apollo-wind/package.json | Adds ./components/custom exports to publish the new public custom shelf. |
| .gitignore | Un-ignores the handwritten guardrails locale TS sources under packages/apollo-wind/.../locales/*.ts. |
Review details
- Files reviewed: 37/38 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| "./components/custom/*": { | ||
| "import": "./dist/components/custom/*.js", | ||
| "require": "./dist/components/custom/*.cjs", | ||
| "types": "./dist/components/custom/*.d.ts" | ||
| }, |
| const [prevLength, setPrevLength] = useState(items.length); | ||
| if (prevLength !== items.length) { | ||
| setPrevLength(items.length); | ||
| setRowIds((prev) => | ||
| prev.length < items.length |
Storybook visual diffBaseline is the deployed main Storybook, so changes merged to main after this branch was last updated can also appear here. Logs Updated (PT): Sep 02, 2026, 01:58:57 AM |
📊 Coverage + size by packagePer-package coverage and bundle size on this PR. New-line coverage = of the source lines this PR adds or changes, the % hit by tests.
"Coverage" is each package's own |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
packages/apollo-wind/package.json:52
- The
./components/custom/*export will expose every top-level module undersrc/components/custom/(e.g.canvas-studio.tsx,chat-*,panel-*), even thoughsrc/components/custom/index.tsexplicitly says only reviewed families should be public. It also likely won’t resolvecomponents/custom/guardrailscorrectly because the built output will bedist/components/custom/guardrails/index.*, notdist/components/custom/guardrails.*.
Consider removing the wildcard export and adding a single explicit ./components/custom/guardrails export instead (root @uipath/apollo-wind already re-exports the guardrails API).
"./components/custom/*": {
"import": "./dist/components/custom/*.js",
"require": "./dist/components/custom/*.cjs",
"types": "./dist/components/custom/*.d.ts"
},
packages/apollo-wind/src/components/custom/guardrails/components/text-list-parameter-field.tsx:39
- Calling
setStateduring render (if (prevLength !== items.length) { ... }) is an anti-pattern and can trigger React warnings, extra renders, or infinite loops (especially under StrictMode’s double-invocation). SyncrowIdsin an effect keyed onitems.lengthinstead of mutating state while rendering.
// Stable per-row ids: without them, removing index N would reuse the DOM node of a
// surviving sibling and swap a focused textarea's content with someone else's. Resync
// during render (React's official pattern) when the `items` array length changes
// out-of-band; the local add/remove handlers below already mutate ids in lockstep.
const [rowIds, setRowIds] = useState<string[]>(() => items.map(() => crypto.randomUUID()));
- Files reviewed: 37/38 changed files
- Comments generated: 1
- Review effort level: Lite
| @@ -0,0 +1,114 @@ | |||
| import { Plus, Trash2 } from 'lucide-react'; | |||
| import { useCallback, useMemo, useState } from 'react'; | |||
| * `getRequiredEmptyParameterIds`. Individual parameters can be replaced via | ||
| * `renderParameter` (e.g. to mount a product model picker for a judge-model parameter). | ||
| */ | ||
| export const GuardrailValidatorForm = forwardRef<HTMLDivElement, GuardrailValidatorFormProps>( |
There was a problem hiding this comment.
This is a second form renderer inside apollo-wind. Neither the PR description nor guardrails/README.md mentions MetadataForm, but src/components/forms/ already owns "definition list → rendered fields": FormSchema → MetadataForm → field-renderer.tsx, with a custom field-type escape hatch and a plugin onValueChange hook for live-edit surfaces. NodePropertyPanel (apollo-react) is the precedent: a properties panel built from node manifests on exactly that stack.
Five of the seven parameter types already map to it: number→number, text→textarea, boolean→switch, enum→select (the host builds the schema, so it can append a stale stored value to options itself), enum-list→multiselect (ui/multi-select.tsx). Only two are real gaps: text-list, which is a generic "array of strings" control worth adding as a first-class field type for everyone, and map-enum, which is guardrails-shaped and is what type: 'custom' + registerCustomComponent exists for. renderParameter duplicates that same seam. The parameters-array ↔ record adapter is real work, but you've already written it (seedGuardrailParameters / syncMapEnumParameters / dropEmptyOptionalParameters).
The cost of the fork is already visible in the field components, because they reimplement ui/form-field.tsx instead of composing it:
ParameterErrorusestext-destructive;FormFieldErrorusestext-errorwith a comment explaining why (--destructiveand--errorresolve differently in several theme blocks intailwind.consumer.css). Guardrail errors will be a different color from every other apollo-wind form error.ParameterLabelrenders a red*;RequiredIndicatordeliberately uses foreground color ("an untouched required field isn't in an error state") and ships ansr-only"(required)" this drops.- The fields pass
className={cn(error && 'border-destructive')}instead ofInput/Textarea'serrorprop, so noaria-invalid/aria-errormessagereaches the DOM. jest-axe won't catch that. space-y-2vs.FormField'sgap-1.5; switch layout mirrored vs.field-renderer'sswitchcase; hand-rolled chips instead ofBadge;h-9/h-8overrides that opt out of thefuture:theme.enum-listandmap-enumcallParameterLabelwithouthtmlFor, so those labels associate with nothing, andParameterErrorhas noaria-liveand isn't linked to any control.
Suggested path: add string-list as a field type and tooltip to BaseFieldMetadata, register map-enum as a custom component (it can read its sibling via useFormContext), and reduce this file to buildGuardrailFormSchema(definitions) + MetadataForm. If the Flow adoption schedule doesn't allow that now, the minimum bar is composing FormField/FormFieldLabel/FormFieldError/RequiredIndicator, using the error props, and MultiSelect for enum-list, with the convergence tracked in the README so the next Guardrails component doesn't extend the fork.
GuardrailBuilder: the complete Add/Edit screen for an OOTB guardrail validator (status banners, usage note, type display, name, description, validator parameters via GuardrailValidatorForm, scope selector with the targeted/addable tools split, action section incl. escalation, evaluations toggle, mixed-scopes banner, Save/Cancel/Save-as-new footer), extracted from Flow Workbench's canvas-local implementation. GuardrailFormLayout ships standalone (inline / inline+hideHeader / modal, secondaryAction, saveDisabled, footerStart region). Escalation's recipient search and app picker are host capabilities injected via render props with usable fallbacks; product URLs never ship in this package. The builder owns its form state (remount to reset) and its validation (catalog messages, labels overrides, optional host errors that merge over internal validation and gate Save); the pure predicates are exported. Locale catalogs extend to the new strings, harvested from Flow's existing translations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
packages/apollo-wind/src/components/custom/guardrails/components/text-list-parameter-field.tsx:44
- This component updates state during render (
setPrevLength/setRowIdsinside the render body). This can trigger React warnings and can behave poorly under StrictMode/double-invocation; the resync should be done in an effect (e.g.,useEffectkeyed onitems.length) or by restructuring state to avoid derived state.
const [prevLength, setPrevLength] = useState(items.length);
if (prevLength !== items.length) {
setPrevLength(items.length);
setRowIds((prev) =>
prev.length < items.length
- Files reviewed: 56/57 changed files
- Comments generated: 4
- Review effort level: Lite
| export function resolveGuardrailFormLocale(locale?: string): GuardrailFormLocale | undefined { | ||
| const raw = locale?.trim().replace(/_/g, '-'); | ||
| if (!raw) return undefined; | ||
| const exact = GUARDRAIL_FORM_LOCALES.find((tag) => tag.toLowerCase() === raw.toLowerCase()); | ||
| if (exact) return exact; | ||
| const base = raw.split('-')[0]?.toLowerCase(); | ||
| return GUARDRAIL_FORM_LOCALES.find((tag) => tag.toLowerCase() === base); | ||
| } |
| if (!allowedKeys) return param; | ||
|
|
||
| // Guard against a null/non-object persisted map before indexing into it. | ||
| const currentMap = param.value && typeof param.value === 'object' ? param.value : {}; |
| <button | ||
| key={option} | ||
| type="button" | ||
| onClick={() => handleToggle(option, !isSelected)} | ||
| className={cn( |
| <button | ||
| key={scope} | ||
| type="button" | ||
| onClick={() => handleToggleScope(scope)} | ||
| className={cn( |
…ystem idiom Token-only colors and primitive reuse across the family, replacing the verbatim Flow-port styling: - Status banners and the usage note become Alert variants (destructive / warning / info) — deletes the only raw Tailwind palette in the family and its hand-written dark: pairs, which ignored the -hc and future themes. The warning banner is an explicit role=status live region. - New GuardrailChip (wind Toggle + cva pill variants incl. an addable dashed appearance, brand-subtle selected state) replaces five hand-rolled chip variants; chips gain focus rings and consistent aria-pressed. FieldShell replaces the duplicated chip wells. Both exported with their variants. - Enum-list parameters with more than eight options now render wind's MultiSelect instead of the hand-rolled popover clone (which clipped overflow chips). - Field anatomy moves to FormField / FormFieldLabel required / FormFieldError: required asterisks follow RequiredIndicator (foreground color + sr-only suffix, per the documented wind decision), error messages move from the theme-unstable destructive token to the error family with aria-live, and Input-based fields adopt the primitive's error prop (aria-invalid / aria-errormessage wired for free). - Shared InfoTooltip gives the evals info icon a real focusable button trigger. Off-scale size overrides collapse onto wind sizes (Button 2xs, Textarea minRows, Select default height); hr becomes Separator; data-slot attributes land on the family's composites. - Additive ui/ change: Textarea and SelectTrigger base classes gain aria-invalid:border-error styling (Input already had it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
There are concrete correctness/consumption issues (notably exports subpath mapping and a strict-typing/indexing bug in syncMapEnumParameters), plus a React anti-pattern (state updates during render) that should be addressed before merge.
Review details
Suppressed comments (3)
packages/apollo-wind/src/components/custom/guardrails/utils.ts:72
syncMapEnumParametersbuildscurrentMapasparam.value ? param.value : {}. That widens the type to{}and makescurrentMap[key]unsafe (and can fail TypeScript strict indexing). If you want the runtime guard, keep the type asRecord<string, number>(and also exclude arrays) before indexing.
packages/apollo-wind/package.json:52- The
./components/custom/*export maps to./dist/components/custom/*.js, but this package outputs nestedindex.jsfiles for folder entrypoints (e.g.src/components/custom/guardrails/index.ts→dist/components/custom/guardrails/index.js). As written, imports like@uipath/apollo-wind/components/custom/guardrailsare likely to fail resolution.
"./components/custom/*": {
"import": "./dist/components/custom/*.js",
"require": "./dist/components/custom/*.cjs",
"types": "./dist/components/custom/*.d.ts"
},
packages/apollo-wind/src/components/custom/guardrails/components/text-list-parameter-field.tsx:44
- This component calls
setPrevLength/setRowIdsduring render whenitems.lengthchanges. Updating state during render is not safe in React (can trigger render loops and breaks concurrent rendering assumptions). Please move the resync logic into an effect keyed onitems.length, or use auseRef-backed id list that you mutate synchronously withoutsetState.
const [rowIds, setRowIds] = useState<string[]>(() => items.map(() => crypto.randomUUID()));
const [prevLength, setPrevLength] = useState(items.length);
if (prevLength !== items.length) {
setPrevLength(items.length);
setRowIds((prev) =>
- Files reviewed: 63/64 changed files
- Comments generated: 0 new
- Review effort level: Lite
What
Adds
GuardrailValidatorFormplus companion types/utils/locale catalogs underpackages/apollo-wind/src/components/custom/guardrails/— the first shared Guardrails component from the Guardrails Apollo Components plan (§7.7 recommended pilot, decision 7.4-A: build once on apollo-wind). Extracted from Flow Workbench'sGuardrailValidatorRuleSection(599 LOC + 593 LOC of tests), which becomes a thin flag-switched adapter over this component ([flow-workbench PR to follow]); Agentsfrontend-swadopts in a later stage via its existing shadow-DOM wind pattern.number,text,boolean,enum,enum-list(inline chips / popover),text-list(stable-row add/remove),map-enum(rows derived from a siblingkeySourceenum-list).parameters/onChange) and validation (errorsmap +onClearError);getRequiredEmptyParameterIdsis the exported predicate hosts use to build the map.renderParameteroverride slot so a product can replace one parameter's editor (e.g. Agents' judge-modelModelPicker, incl. sidecar-parameter writes viaonParametersChange).seedGuardrailParameters,syncMapEnumParameters(mirrors the map-enum editor's key resolution),dropEmptyOptionalParameters.optionsrenders as a synthetic option instead of blanking; error messages render on all seven types; label/input associations added (jest-axe clean).This component ships its own chrome strings (2 placeholders, "Add", 2 aria-labels — domain strings stay host-resolved) as TypeScript locale catalogs for 14 languages, loaded lazily via a
localeprop with per-key English fallback and alabelsoverride prop. It followsap-model-picker'sloadMessagesmechanism but framework-free (no lingui, no compile step); translations were harvested from Flow Workbench's existing catalogs, so no new translation work. LOCALIZATION_GUIDE prescribes props-based localization — treat this PR as the ratification vehicle for component-owned catalogs. If declined, droppinglocales/+ the loader is non-breaking (thelabelsprop remains a complete path).Also note: this wires
components/custominto the public API for the first time (package.jsonexports + barrels). Only the guardrails family is exported; the 25 pre-existing prototype modules incustom/stay private. A.gitignorenegation was added because the root**/locales/*.tsrule (aimed at lingui-compiled artifacts) would otherwise ignore these hand-written catalogs.Update (phase 2): the whole Add/Edit guardrail screen
The family now also ships
GuardrailBuilder(the complete Add/Edit screen: status banners, usage note, type display, name, description, validator parameters, scope selector with the targeted/addable tools split, action section incl. escalation, evaluations toggle, mixed-scopes banner, Save/Cancel/Save-as-new footer) andGuardrailFormLayout(the shell, standalone: inline / inline+hideHeader / modal,secondaryAction,saveDisabled,footerStart). Extracted from Flow'sBuiltInGuardrailBuilder(415 LOC) and consumed behind the same flag (flow-workbench PR below).renderRecipientSearch(directory autosuggest) andrenderAppPickerare host capabilities injected as render props with usable fallbacks;escalateHelprenders the host's marketplace line, so product URLs never ship here.labels), and an optional flaterrorsprop merges over internal validation (host wins per field) for hosts that validate externally (Agents' zod flow at stage 2). The pure predicates (getGuardrailActionErrorFields,getGuardrailSelectorErrorFields) are exported.builder-types.ts(GuardrailDefinition,GuardrailBuilderValue,GuardrailActionincl. escalate shapes,GuardrailRecipientTypeas a const object); mutual assignability with Flow'''s wire types is pinned by a type-assertion file on the Flow side.keyto reset (every known host already remounts per session).Alert(role=alerton the error tone).Update (phase 3): wind-idiom restyle
The family now follows the design-system rules end to end; the verbatim-from-Flow styling is gone. Visual deltas for reviewers (all flag-gated in consumers): required asterisks are foreground-colored per
RequiredIndicator'''s documented decision (with an sr-only "(required)" suffix); the three status banners + usage note areAlertvariants on the error/warning/info token families (the family'''s only raw Tailwind palette and hand-writtendark:pairs are deleted — they ignored the -hc/future themes); chips are a sharedGuardrailChip(windToggle+ cva pill variants, brand-subtle selected state, focus rings, consistentaria-pressed); enum-list parameters with >8 options render wind'''sMultiSelectinstead of a hand-rolled popover clone; error messages move fromdestructiveto theerrortoken family viaFormFieldError(aria-live), andInput-based fields adopt the primitive'''serrorprop; field anatomy isFormField/FormFieldLabel; sizes collapse onto wind'''s scales (Button2xs,Textarea minRows, Select default height);hr→Separator.Two additive ui/ edits:
TextareaandSelectTriggerbase classes gainaria-invalid:border-error aria-invalid:focus-visible:ring-error(Inputalready had them). Full suite re-run green (1618 tests).Conventions proposed for
custom/with this PR (new for the directory, universal inui/): cva variant objects exported alongside components (guardrailChipVariants), anddata-slotattributes on composites. Also flagged:RequiredIndicator'''s sr-only suffix is English-only (could take an i18n prop later), andui/multi-select.tsxhas a pre-existing nested-<button>React warning (Badge remove-button inside the trigger button) that this PR surfaces but does not touch.Testing
renderParameterslot incl. sidecar writes,onClearErrorordering, locale catalog loading for all 14 locales, labels override, stale-enum option). Coverage on the new files: 94% statements / 82% branches / 93% functions.jacatalog.🤖 Generated with Claude Code