feat(apollo-react): add-guardrail palette [AL-576] - #1147
andreizdrali-uipath wants to merge 14 commits into
Conversation
|
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
Unresolved critical and moderate findings affect selection safety, key identity, error-state rendering, and accessibility.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds a reusable Apollo React guardrail palette for built-in and BYO guardrails, with localization, grouping utilities, status chips, stories, and tests.
Changes:
- Adds generic palette rendering, types, callbacks, and grouping/identity helpers.
- Adds localized palette labels and catalog coverage.
- Adds shared status-chip UI and supporting fixtures/tests.
File summaries
| File | Review summary |
|---|---|
pnpm-lock.yaml |
Reviewed; no final comment. |
packages/apollo-wind/src/index.ts |
Reviewed; no final comment. |
packages/apollo-wind/src/components/ui/textarea.tsx |
Reviewed; no final comment. |
packages/apollo-wind/src/components/ui/select.tsx |
Reviewed; no final comment. |
packages/apollo-wind/src/components/ui/info-tooltip.tsx |
Reviewed; no final comment. |
packages/apollo-wind/src/components/ui/info-tooltip.test.tsx |
Reviewed; no final comment. |
packages/apollo-wind/src/components/ui/index.ts |
Reviewed; no final comment. |
packages/apollo-wind/src/components/forms/validation-converter.ts |
Reviewed; no final comment. |
packages/apollo-wind/src/components/forms/string-list-field.tsx |
Reviewed; no final comment. |
packages/apollo-wind/src/components/forms/metadata-form.stories.tsx |
Reviewed; no final comment. |
packages/apollo-wind/src/components/forms/index.ts |
Reviewed; no final comment. |
packages/apollo-wind/src/components/forms/form-schema.ts |
Reviewed; no final comment. |
packages/apollo-wind/src/components/forms/field-renderer.tsx |
Moderate (1 vote): associate the MultiSelect label and control for an accessible field name. |
packages/apollo-react/src/test/setup.ts |
Reviewed; no final comment. |
packages/apollo-react/src/i18n/index.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/index.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/utils.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/use-guardrail-definitions.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/types.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/render-parameter-bridge.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/palette-types.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/index.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/i18n.test.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/guardrail-validator-form.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette.tsx |
Moderate (1 vote each): suppress the empty state when a load error exists and distinguish rendered group keys to avoid collisions. |
packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette.test.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette.stories.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette-utils.ts |
Moderate (2 votes each): make item IDs collision-free and disambiguate synthetic and user-provided group keys. |
packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette-utils.test.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/guardrail-form-layout.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/guardrail-form-layout.test.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/guardrail-form-layout.stories.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/form-schema-builder.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/definitions-wire.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/definitions-parse.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/definitions-parity.test.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/definitions-copy.test.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/parameter-label.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/mixed-scopes-banner.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/mixed-scopes-banner.test.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/map-enum-field.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-chip.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-chip.test.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-banner.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-banner.test.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-scope-selector.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-palette-item.tsx |
Critical (1 vote): prevent spread props from overriding the guarded onClick for unauthorized entries. |
packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-palette-item.test.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-chip.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-chip.test.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-action-section.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-action-section.test.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/field-shell.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/field-shell.test.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/components/enum-list-chips-field.tsx |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/builder-utils.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/builder-types.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/host-copy-baselines.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/guardrail-palette.fixtures.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/definitions-wire.fixtures.ts |
Reviewed; no final comment. |
packages/apollo-react/package.json |
Reviewed; no final comment. |
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (3)
packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette.tsx:86
- When a load fails before any definitions are available,
erroris truthy butisEmptyis still true, so the palette renders both “Failed to load built-in validators” and “No guardrails available.” The latter incorrectly presents a transport failure as a valid empty catalog; suppress the empty state when an error is present while preserving stale definitions under the banner.
packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette.tsx:118 group.keyis used directly as the React key, but named BYO groups take their key from user/connection data while__uipath__is reserved for the synthetic UiPath group. A valid folder or connector named__uipath__therefore produces duplicate React keys, which can cause unstable reconciliation when the catalog changes. Namespace the rendered key by group kind and distinguish the unnamed BYO group instead of usinggroup.keyalone.
packages/apollo-wind/src/components/forms/field-renderer.tsx:474MultiSelectdeliberately uses itsidto allow a<label>to name the trigger, but this renderer omits both sides of that association. As a result, enum-list fields rendered through this path (including guardrail lists with more than eight options) are announced as the generic “Select options...”/selection count instead of their field label. Pass the field name to both the label and the control, and cover the accessible name in the renderer test.
- Files reviewed: 93/94 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| return definition.byoValidatorName === undefined | ||
| ? definition.validator | ||
| : `${definition.byoValidatorName}:${definition.byoGuardrailConnectionId ?? ''}`; |
| .map(([key, list]) => ({ | ||
| // A BYO definition with neither a folder nor a connector name has nothing to head its | ||
| // group with; both products still render it, unheaded. | ||
| key: key ?? '__byo__', |
📊 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 |
|
Published previews, both green:
Host sessions want #1148's pair: this PR's preview has no list, and #1140's has no palette. CI: Test Coverage, Typecheck, Lint, Format and Build green. The two red |
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 14, 2026, 09:06:10 AM |
27643cd to
03bb89c
Compare
Integration branch only: it exists so a host can pin one preview package carrying every open apollo stream. Not for merging into main. Rebuilt on 2026-09-11 after the whole stack moved onto #1138's current head (`d658731b`) and picked up a first pass of review fixes on #1139 and #1140. Reset to `feat/apollo-react-guardrail-list` and re-merged `feat/apollo-react-guardrail-palette` (#1147, AL-576). The chip files both branches carry merged clean, being byte-identical again. Of the 17 conflicts, `i18n.ts`, `i18n.test.ts` and the 13 locale catalogs are unchanged on both sides since the previous merge (`8856e4e0`), so its resolution was reused verbatim. `index.ts` is the union of both barrels, biome-sorted, and checked for a lost export. The README was rebuilt from `8856e4e0`'s merged copy with the three deltas since then reapplied (the new base's, #1139's review fixes, #1140's review fixes), all cleanly, so the sections still run along the data flow: definitions layer, list, palette, builder. Verified after the merge: Guardrails suite 453 passing (26 files), tsc and biome clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Four moderate findings remain involving invalid chip markup, key collisions, and an unintended React/i18n dependency.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (3)
packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-palette-item.tsx:50
GuardrailStatusChiprenders the windBadge, which is a<div>, but these chips are inserted into the<span>at this line and ultimately inside the palette's<button>. That produces invalid<button><span><div>…markup (and ReactvalidateDOMNestingwarnings) for every definition entry; render the status chip as phrasing content or change the item structure so no div-based badge is nested in the button.
<span className="flex flex-wrap items-center gap-1.5">
<span className="text-sm font-medium">{name}</span>
{chips}
packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette-utils.ts:83
- These keys are not namespaced by group kind, so a customer-controlled
folderPathor connector name of__uipath__creates the same React key as the trailing UiPath group (and__byo__collides with the nameless BYO group). The palette can then emit duplicate-key warnings and reconciliation can reuse the wrong group; prefix BYO keys separately from the synthetic UiPath key (and update the helper's key contract/tests).
packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette-utils.ts:2 - This helper is documented as pure/React-free, but importing
isByoGuardrailDefinitionas a runtime value fromdefinitions-enrichpulls in itsGUARDRAIL_COPY_ENdependency and thereforedefinitions-copy.ts, which imports React anduseSafeLingui. A caller that only needs grouping now loads the full copy/i18n graph (and cannot use this as a genuinely non-React utility). Move the BYO predicate to a small dependency-free module shared by both files, or otherwise avoid this runtime dependency.
- Files reviewed: 110/111 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
One or more issues must be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (4)
packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-palette-item.tsx:60
- The
chipsslot is inserted inside the palette item's<button>, butGuardrailStatusChipdelegates to windBadge, which renders a<div>. This produces flow-content<div>elements inside both a button and the surrounding<span>, violating their phrasing-content models and potentially producing inconsistent accessibility trees. Make the status chip render as a phrasing element such as<span>, or change the composition so it is not nested inside those elements.
<span className="flex flex-wrap items-center gap-1.5">
<span className="text-sm font-medium">{name}</span>
{chips}
</span>
packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-chip.tsx:30
Badgerenders a<div>, so this status chip becomes a<div>inside the<button>emitted byGuardrailPaletteItem(and inside its<span>chip row). That violates the button/phrasing-content contract and can produce invalid DOM and accessibility behavior for every connector, unauthorized, or preview chip. Use a span-based status-chip implementation or render these chips outside the button; mirror the shared component change in the sibling list PR.
<Badge
ref={ref}
variant={TONE_VARIANT[tone]}
data-slot="guardrail-status-chip"
className={cn('h-5', GUARDRAIL_CHIP_GEOMETRY, className)}
packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette-utils.ts:75
- The reserved synthetic keys are not namespaced from named BYO keys. A valid
folderPathor connector name of__uipath__collides with the trailing UiPath group's key, and__byo__can collide with the nameless BYO group; React then receives duplicate keys and may reconcile the wrong group when the catalog changes. Namespace all named BYO keys (and update the utility tests), or derive the render key from the group kind rather than the raw folder/connector name.
packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette.tsx:80 activeEntryis an array index, but the entries can be reordered or have the create-custom item inserted/removed while the palette is mounted. In that case the focused button keeps focus while a different button receivestabIndex=0, so re-entering with Tab no longer returns to the previously focused entry, contrary to the documented roving-focus contract. Track the active entry by its stable item id (or reconcile the index against the current entry list) when the catalog changes.
- Files reviewed: 110/111 changed files
- Comments generated: 1
- Review effort level: Lite
… with #1140 wind's `Badge` renders a `<div>`, and the palette entry puts these chips inside its `<button>`, where flow content is invalid. Compose from wind's exported `badgeVariants` on a `<span>` instead: same classes, an element that may live there, `ComponentPropsWithoutRef<'span'>` and `HTMLSpanElement` on the ref. The chip is byte-identical on #1140 and #1147, so this commit lands on both.
There was a problem hiding this comment.
🟡 Changes recommended
One or more issues must be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (4)
Previously missed (1) — in code that hasn't changed since the last review.
packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette-utils.ts:1
- This utility is documented and exported as React-free, but importing
isByoGuardrailDefinitionfromdefinitions-enrichpulls indefinitions-copy's runtimeuseMemo/useSafeLinguidependency. A non-React caller ofgroupGuardrailsForPalettetherefore still loads React and can fail when the peer is absent. Keep the BYO predicate in a value-only module or define it locally in this utility.
packages/apollo-react/src/canvas/components/Guardrails/form-schema-builder.ts:86
buildFieldValidationis attached to every guardrail field here, butGuardrailValidatorFormrunsMetadataFormwithschemaMode: 'onChange'and this version has nodisableValidationprop. Required/range resolver errors consequently appear while editing even though the documented guardrail contract is validation-free and hosterrorsare the source of truth. Remove these validation rules from this adapter or add and actually pass an explicit validation opt-out.
packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette-utils.ts:88- Folder and connector names are arbitrary strings, but the grouping code reserves
__uipath__and__byo__as synthetic keys. A BYO folder named__uipath__collides with the trailing UiPath group (and a__byo__folder collides with the nameless BYO group), producing duplicate React keys and unstable group reconciliation. Namespace synthetic and user-derived keys so the domains are disjoint.
packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette.tsx:80 - The roving position is stored as a numeric index, so it no longer identifies the item when the palette changes. For example, if
onCreateCustomis added while the second definition is focused,activeEntrystays1; the previously focused definition shifts to index2and getstabIndex={-1}, while a different item becomes the tab stop. The same happens when refreshed/localized definitions reorder. Track the active item's stable key (with a fallback when it disappears) and derive its current flat index so the documented return-to-last-focused-item behavior is preserved.
- Files reviewed: 110/111 changed files
- Comments generated: 1
- Review effort level: Lite
|
|
||
| return schema.optional(); | ||
| return schema.superRefine((value: unknown, ctx: z.RefinementCtx) => { | ||
| if (!RulesEngine.evaluateExpression(expression, { value })) { |
… with #1140 wind's `Badge` renders a `<div>`, and the palette entry puts these chips inside its `<button>`, where flow content is invalid. Compose from wind's exported `badgeVariants` on a `<span>` instead: same classes, an element that may live there, `ComponentPropsWithoutRef<'span'>` and `HTMLSpanElement` on the ref. The chip is byte-identical on #1140 and #1147, so this commit lands on both.
8b65af8 to
878a293
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
The moderate renderer identity issue can reset chat tool-call and trace state.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
packages/apollo-react/src/material/components/ap-chat/components/message/chat-message-content.tsx:213
getApolloMessageRenderers(locale)creates new inline component types for the tool-call and tree renderers on every render. When a message is updated (or this component re-renders for an unrelated reason),ApolloMessageRenderertherefore changes type and React unmounts/remounts the renderer subtree, resettingApToolCall's expanded state and nested trace state. Memoize the renderer list bylocalebefore the conditional return, or move these renderer components to stable module-scope definitions, so message updates preserve their state.
- Files reviewed: 109/110 changed files
- Comments generated: 0 new
- Review effort level: Lite
…airs Generic forms/ and ui/ enablers extracted from the guardrails work. The guardrails domain family itself lives in apollo-react (#1138, stacked on this). Squashed from 20 commits at review request: most were iterations on each other, and two cancelled out entirely (the controlled-host seam was added and then removed during review), which would otherwise have cut a major release for props that no release ever shipped. New in the forms layer: - `string-list` field type — repeated rows with Add/Remove (`maxItems`, `maxLength`, stable row ids), zod conversion, and `formatTemplate`. Not yet offered by FormDesigner; the reason is documented at FIELD_TYPE_METADATA. - `tooltip` / `tooltipAriaLabel` field metadata rendered by `FormFieldLabel`, plus textarea `minRows`/`maxLength` and multiselect `emptyMessage` / `searchPlaceholder`. - `container: 'form' | 'div'` for embedding inside a host's own chrome: submit actions become plain buttons wired to the form's handler, and Enter is swallowed for single-line inputs so it cannot trigger the host form's implicit submission. - `InfoTooltip` promoted into `components/ui`, with `FormFieldLabel` owning the composition so call sites stop reassembling label + indicator + tooltip. - `MetadataFormProps`, `useWatch` and `CustomValueType` exported, so cross-package custom fields share one react-hook-form instance and can declare their value shape. Repaired — declared in the schema contract but never implemented: - `ValidationConfig.custom` was typed, documented and serialized, but the converter never read it. Now enforced. `RulesEngine.tryEvaluateExpression` distinguishes "the evaluator threw" from "the expression returned falsey", so an expression it cannot handle is not enforced rather than pinning the field permanently invalid. Its scope is documented as single-field; cross-field logic belongs in `rules`. - `FormPlugin.components` was typed and never read, which is why hosts registered asynchronously and missed the first paint. - `plugin.onValueChange` sat behind a mount-lifetime gate that could swallow a plugin's first keystroke. - Custom fields validated as `z.any()`, where `required` and `minItems` are no-ops; they can now declare a `valueType`. Correctness and accessibility: - A required string field rejected `' '` on one path and accepted it on the other. Both now use `isEmptyFieldValue`, as a `.refine` rather than `.trim().min(1)`, which would mutate the submitted value. Behaviour change for anyone who relied on whitespace satisfying `required`, called out in the PR description. - `required` was defeated by an explicit `minItems: 0`. - `aria-invalid` is forwarded by every control that can render an error, with matching invalid styling on Select and Textarea. Radix renders the slider thumb and the datetime trigger as the interactive element, so both take the invalid state and an `aria-labelledby` explicitly — a neighbouring <label> reaches neither — and the radio group had no accessible name of its own. - `StringListField` receives the Controller ref and attaches it to the first row, so react-hook-form's `shouldFocusError` can reach it on a failed submit like every other built-in control. - A custom field declaring `valueType: 'number'` now gets the numeric constraints: `applyNumberConstraints` only saw `fieldType`, so `min`, `max` and `integer` were silently dropped and `{ min: 1 }` still accepted 0. `buildZodSchemaFromFields` accepts and forwards `valueType` too — it is exported, and without it a whole-form schema built through that helper validated every typed custom field as `z.any()`. - Label/control association (`htmlFor` + `id`) across the renderer. - The schema serializer carries the new field metadata, so a round-trip no longer drops it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Guardrails UI, shared by Flow and Agents, as an MUI-free family under `src/canvas/components/Guardrails` and exported through the narrow `@uipath/apollo-react/canvas/guardrails` subpath. Members: `GuardrailBuilder` (the whole Add/Edit screen), `GuardrailFormLayout` (the screen shell), and `GuardrailValidatorForm` (the validator parameter section). Built on apollo-wind's forms/ MetadataForm stack rather than its own renderer: five of the seven parameter types map onto first-class field types, while the chip-style enum-list, `map-enum` and host `renderParameter` overrides register as custom components. Strings localize through lingui (`guardrails.*` ids, 14 catalogs). Squashed from 7 commits at review request, matching #1107. MetadataForm owns its own state, so this family's controlled contract is translated onto its plugin seam in exactly one named place, `useMetadataFormBridge`: it registers the custom components from the first paint, pushes host values in structurally compared (so an echo of the form's own emission performs no write and focus survives), pushes host errors in as `type: 'external'`, and suppresses its own echo while writing. A sync arriving before `onFormInit` is replayed rather than dropped. Validation is shared, and the split is deliberate: the schema declares `required`/`min`/`max` from the definitions with messages from the label catalog, so they translate; the host owns domain rules and the save-time gate through `getRequiredEmptyParameterIds` / `getOutOfRangeParameterIds`. Where the two disagree the host's verdict is what renders — a `text-list` of whitespace-only rows passes the array's `.min(1)` but counts as empty for the host predicate — and a test pins that rather than leaving it to whichever ran last. Custom fields declare a `valueType` so those constraints bind to them too; `map-enum` has no counterpart shape, so its required check stays the host's alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The chat renderer identity issue remains unresolved, along with three title-less alert alignment comments.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (4)
packages/apollo-react/src/canvas/components/Guardrails/components/escalate-action-fields.tsx:256
- This fallback alert is title-less, so the default
AlertDescriptionmt-1places its text below the icon attop-3.5. Add themt-0override used by the other title-less guardrail banner.
<AlertDescription>{labels.appPickerUnavailable}</AlertDescription>
packages/apollo-react/src/canvas/components/Guardrails/components/mixed-scopes-banner.tsx:21
- This title-less alert inherits
AlertDescription'smt-1even though the icon is positioned attop-3.5, leaving the mixed-scope message lower than the icon. Add themt-0override here, as inGuardrailStatusBanner, to keep the alert contents aligned.
<AlertDescription>
packages/apollo-react/src/canvas/components/Guardrails/guardrail-builder.tsx:309
- This alert has no
AlertTitle, butAlertDescriptiondefaults tomt-1while the icon is positioned attop-3.5. The usage-note text is therefore visibly offset below its icon, so apply the samemt-0override used byGuardrailStatusBanner.
packages/apollo-react/src/material/components/ap-chat/components/message/chat-message-content.tsx:210 - This factory creates fresh anonymous component functions on every render, so
ApolloMessageRendererchanges type wheneverAutopilotChatMessageContentre-renders. React then unmounts and remountsApToolCall/ApolloChatTreeRenderer, resetting local expansion and other UI state; memoize the renderer list for the current locale (or move the wrapper components to module scope) so their identities remain stable.
- Files reviewed: 109/110 changed files
- Comments generated: 0 new
- Review effort level: Lite
Apollo maps `--accent` to `--surface-hover` and `--muted` to `--surface-overlay`, the raised panel these sections sit on, so the row was hovering to the colour of its own background. The palette item in #1147 already uses `accent`. The activatable row body also gains the explicit `focus-visible` ring that the palette item and the centralized row carry, instead of falling back to the UA outline, and the README states the rule the three of them now share: hover is never a prop, an element that is itself a control gates it on being enabled and pairs it with a cursor and a ring, and a row that merely contains controls highlights unconditionally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…578] Apollo maps `--accent` to `--surface-hover` and `--muted` to `--surface-overlay`, the raised panel this section sits on, so a hovered row was painting itself the colour of its own background. The palette item in #1147 already uses `accent` and the list row in #1140 moves with this. The README gains the rule the three rows share, in the wording #1140 carries, so the two branches merge to one copy: hover is never a prop, an element that is itself a control gates it on being enabled and pairs it with a cursor and a ring, and a row that merely contains controls highlights unconditionally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ilDefinitions Adds the definitions layer the guardrails family was missing: the seam between the `/api/execution/guardrails/definitions` payload and the `GuardrailDefinition`s `GuardrailBuilder` renders. Flow and Agents each carry their own copy of this today, and the two have drifted. - `definitions-wire.ts` mirrors the payload as hand-written types, reusing `GuardrailScope` and `GuardrailDefinitionStatus` from `builder-types` so the wire and display layers cannot diverge. It admits both products' nullability variants. - `definitions-parse.ts` validates unknown input and never throws: a non-array payload sets `inputError`, a bad definition is dropped whole and reported in `invalid`, unknown keys are stripped. zod is private to this module, pinned to the public mirror by a bidirectional assignability check on the hot path, a runtime key-set assertion and a source-level guard, so the folder's emitted declarations carry no schema types. - `definitions-copy.ts` holds the canonical copy for the six built-in validators as 63 lingui messages in the shared canvas catalog, keyed by raw wire values. Translations harvested from both products, 62 of 63 in each of the 12 locales. - `definitions-enrich.ts` resolves that copy onto the wire shape. Pure and React-free; `EnrichedGuardrailDefinition extends GuardrailDefinition`, so its output feeds the builder with no mapping. - `useGuardrailDefinitions` composes the three over `useState` + `fetch` + `AbortController`, following `useDiscoveryModels`. `options.definitions` skips the request entirely, which is how each product keeps its own transport. Unlike `useDiscoveryModels` the context is compared by content, not identity: keying the effect off identity made an inline context object refetch on every render without terminating. Where the products' English differed, all 17 choices are declared with a reason in `definitions-parity.test.ts` and asserted against both products' transcribed copy, so the shared table cannot quietly drift from the tables it replaces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…AL-574] Review of #1139 against the #1107/#1138 threads (plan/review-2026-09-11 §3.2). - Type the fetch mocks as `vi.fn<typeof fetch>`: `vi.fn(async () => ...)` infers a zero-parameter mock, so every `mock.calls[i]?.[1]` assertion was a TS2493/TS2339 under the repo's strict config. CI cannot see it (tests are excluded from `tsc` and biome does not typecheck), so it is checked with a throwaway tsconfig. - `loading` starts `true` when the hook is about to fetch, so a host rendering `loading ? <Spinner/> : <Empty/>` no longer flashes the empty state on first paint. - `refetch` is a no-op while the hook is disabled. It used to issue a real request whose result `parsed` then discarded in favour of `options.definitions`. - JSDoc and README: `options.definitions` is compared by identity (pass a stable reference), a failed request keeps the previous results, and the zod boundary is pinned by a source-level check plus two tests, not by shipped runtime assertions. - Name the map-enum `0..1` step `0.1` default as a product assumption and pin what keeps it safe: after the rebase onto `d658731b`, `min`/`max` are enforced through `validation` in `onChange` mode, but both that path and `getOutOfRangeParameterIds` are number-only, so a synthesized map-enum bound cannot reject a threshold map whose real range is different (harmful content is 0..6). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… enforces [AL-574] `getOutOfRangeParameterIds` used to look at `number` parameters only, so the 0..1 step 0.1 this layer synthesized for an unbounded threshold map was an editor hint nothing could reject a value against. The guardrails family has since widened that check to `map-enum` and hosts gate Save on it, which turns a bound nobody stated into a blocked save on a scale the backend never published. Pass the wire's `min`/`max` through when it sends them and leave them off when it does not, so only real constraints reach the check. `step` stays: neither the check nor `buildFieldValidation` reads it, and without it a 0..1 score steps by 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Promote the read-only status chip and the chip geometry constant it reuses. Copied verbatim from the AL-575 list branch (#1140), which introduced them: the palette needs the same chip for an unauthorized definition, and both PRs branch off the definitions layer rather than stacking, so each carries the shared files and whichever merges second drops its duplicate on rebase. Keep the two copies byte-identical.
… with #1140 wind's `Badge` renders a `<div>`, and the palette entry puts these chips inside its `<button>`, where flow content is invalid. Compose from wind's exported `badgeVariants` on a `<span>` instead: same classes, an element that may live there, `ComponentPropsWithoutRef<'span'>` and `HTMLSpanElement` on the ref. The chip is byte-identical on #1140 and #1147, so this commit lands on both.
…[AL-574] Every guardrails component's i18n test re-implements the same three checks over its own id prefix: English parity, orphaned ids, and translation coverage. The catalog is hand-authored and harvested by a one-off script, so these scans stand in for `lingui extract` and a translation pipeline, and each new component copy-pasted them. They move to `__fixtures__/catalog-coverage` as reporting functions, so a failure still points at the calling test's own line. The definitions layer is the first caller and gains what it was missing: the orphan sweep now covers all thirteen catalogs rather than English alone, and a coverage check that pins the one harvested-English-only entity label instead of leaving the gap invisible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…AL-574] The centralized section (#1161) needs the green both products already give the BYO origin chip, and had forked its own copy of the chip to get it. The tone belongs where the chip lives, so every consumer gets the same four. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add `GuardrailPalette`, the picker both products open from their guardrails list: grouped definitions, an optional create-custom entry, and the loading, failed and empty states. The host filters and the palette offers. Definitions arrive pre-filtered (flags, entitlements, `FeatureDisabled`/`Disabled`, Tool scope), both callbacks are intents, and no telemetry, flag or product type crosses the boundary. Definitions are generic over the eight fields the palette reads, so `EnrichedGuardrailDefinition` and a product's own type both go in and come back out of `onSelectOotb` unchanged. Grouping and entry identity are re-derived from what both products already ship: one unheaded group in payload order without BYO definitions, otherwise a group per BYO folder or connector plus a trailing UiPath group, and BYO entries keyed by validator name and connection id. An `Unauthorised` definition is offered, chipped and not choosable (`aria-disabled`, so it stays reachable), which is Flow's behaviour; Agents lets it through to a builder that then refuses to save. Only the picker ships. Flow's dialog and inline overlay and Agents' sidebar takeover are host orchestration, shown in the stories rather than modelled in a wrapper. Nine `guardrails.palette.*` lingui ids, all harvested from the two products' own catalogs at 9 of 9 coverage in the 12 translated locales, asserted both ways against the shared canvas catalog. Three label resolvers now share one `mergeLabels` helper.
Typed the `renderPalette` test helper through a `GuardrailPaletteProps` object instead of casting the partial on the spread, which was the only strict-tsc error in this PR (two TS2783, invisible to CI since it does not typecheck tests). Moved the palette entry onto wind's own interactive-item idiom: hover is `bg-accent`/`text-accent-foreground` and gated on the enabled branch, so the `hover:bg-transparent` counter-rule is gone; disabled is `opacity-50`; the focus ring gains `ring-offset-background` and `focus-visible:ring-offset-2` like `Button`; the leading icon is `text-muted-foreground` like the rest of the family. `onClick` is omitted from the props type alongside `onSelect` so a spread cannot reinstate a handler past the disabled guard. Nested groups now name themselves with `aria-labelledby` pointing at the visible header rather than repeating it in `aria-label`, and the palette is a single tab stop with roving focus: ArrowUp/ArrowDown/Home/End move across group boundaries, the create-custom entry is first in that order, and `aria-disabled` entries are included so the Unauthorised chip stays reachable. Two tests cover it. README gains the keyboard model, the `<output>` loading line and the token choices; the palette-utils JSDoc says the sort follows the runtime locale.
… [AL-576]
Locate the focused entry with the keyboard event target rather than
`document.activeElement`. Agents renders the family inside a shadow root, where
`document.activeElement` retargets to the shadow host, so the lookup never
matched and Arrow/Home/End were a silent no-op in one of the two hosts. A new
test renders into `host.attachShadow({ mode: 'open' })` and arrows down; it
fails against the previous handler.
Also: the i18n orphan scan covers `ru` (deliberately empty is not licence to
keep a stale id; the coverage check still excludes it), and the palette-utils
JSDoc says "pure functions, callable outside React" rather than claiming a
React-free module, which the import graph is not.
The three catalog checks moved to `__fixtures__/catalog-coverage` on the definitions layer, where every component's i18n test can call them instead of carrying its own copy of the locale list and the file reader. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n [AL-576] Wind's `AlertDescription` is unconditionally `mt-1`, which assumes an `AlertTitle` sits above it, while `alertVariants` pins the icon at `[&>svg]:top-3.5`. An alert with no title therefore renders its text 4px below the icon, visible here in the `Load Failed` story. `mt-0` at each title-less call site, the same hunk as apollo-ui#1161 and apollo-ui#1140 so the three branches merge without a conflict. The real fix belongs in apollo-wind (make the offset conditional on a preceding title); this is the local workaround until that lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Apollo maps `--accent` to `--surface-hover` and `--muted` to `--surface-overlay`, the raised panel these sections sit on, so the row was hovering to the colour of its own background. The palette item in #1147 already uses `accent`. The activatable row body also gains the explicit `focus-visible` ring that the palette item and the centralized row carry, instead of falling back to the UA outline, and the README states the rule the three of them now share: hover is never a prop, an element that is itself a control gates it on being enabled and pairs it with a cursor and a ring, and a row that merely contains controls highlights unconditionally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
e7e1230 to
596e3e3
Compare
…578] Apollo maps `--accent` to `--surface-hover` and `--muted` to `--surface-overlay`, the raised panel this section sits on, so a hovered row was painting itself the colour of its own background. The palette item in #1147 already uses `accent` and the list row in #1140 moves with this. The README gains the rule the three rows share, in the wording #1140 carries, so the two branches merge to one copy: hover is never a prop, an element that is itself a control gates it on being enabled and pairs it with a cursor and a ring, and a row that merely contains controls highlights unconditionally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Unresolved moderate accessibility and React-key issues remain in the final review comments.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (7)
packages/apollo-react/src/canvas/components/Guardrails/components/escalate-action-fields.tsx:256
- This title-less fallback alert inherits
AlertDescription'smt-1, leaving its message lower than the info icon. Add themt-0override used for the other title-less guardrail alerts.
<AlertDescription>{labels.appPickerUnavailable}</AlertDescription>
packages/apollo-react/src/canvas/components/Guardrails/components/escalate-action-fields.tsx:182
- When neither host render prop supplies the recipient control, this label is a sibling of fallback
<Input>elements that have noidand noaria-label; the placeholder is not an accessible name. Add a stablehtmlForand matchingidto both fallback inputs (or expose an equivalentaria-labelledby) so the fallback remains usable without host capabilities.
<Label>
{recipientTypeLabels[displayedRecipientType] ?? labels.recipientFallbackLabel}
<RequiredIndicator />
</Label>
packages/apollo-react/src/canvas/components/Guardrails/components/escalate-action-fields.tsx:205
- The searchable fallback marks the input invalid, but its
FormFieldErroris not referenced byaria-describedby(andInput's automatic error wiring is bypassed because the error is rendered outside it). Assistive-technology users therefore get the invalid state without the recipient error message; give the error an id and reference it from this input.
aria-invalid={errors?.recipient ? true : undefined}
/>
)}
<FormFieldError>{errors?.recipient}</FormFieldError>
packages/apollo-react/src/canvas/components/Guardrails/components/mixed-scopes-banner.tsx:21
- Because this alert has no
AlertTitle, the defaultAlertDescriptionmargin offsets the first paragraph below the icon. Add the title-lessmt-0override, as inGuardrailStatusBanner, so this mixed-scope banner aligns consistently.
<AlertDescription>
packages/apollo-react/src/canvas/components/Guardrails/components/mixed-scopes-banner.tsx:28
- These keys share one
<ul>even though they come from two arrays. A tool can have the same name as a scope (or a duplicate tool name), producing duplicate React keys and incorrect reconciliation when the applied-scope data changes. Prefix the keys by kind and include the array index (or another guaranteed-unique identity).
{otherAppliedScopes.scopes.map((s) => (
<li key={s}>{s}</li>
))}
{otherAppliedScopes.tools.map((tool) => (
<li key={tool}>{tool}</li>
packages/apollo-react/src/canvas/components/Guardrails/components/parameter-label.tsx:25
InfoTooltiprenders a real<button>, so placing it inside this native<label>creates invalid label content and makes tooltip activation participate in the parameter control's label activation. Keep the tooltip beside the label in an inline wrapper, asFormFieldLabeldoes, while retaining it inside the headerdivforasTextHeader.
{paramDef.tooltip && (
<InfoTooltip content={paramDef.tooltip} aria-label={labels.moreInformation} />
)}
packages/apollo-react/src/canvas/components/Guardrails/guardrail-builder.tsx:309
- This alert has no
AlertTitle, butAlertDescriptiondefaults tomt-1, so the usage-note text is rendered 4px below the icon. Apply the same title-less override used byGuardrailStatusBannerso the note is vertically aligned.
- Files reviewed: 108/109 changed files
- Comments generated: 0 new
- Review effort level: Lite
Review only the last five commits (
41abd5f5the palette,7f07afd7the first round ofreview fixes,
08e76fc7the second pass,878a2934the shared catalog scans,e7e12307thetitle-less alert offset). The eight
below them are #1107, #1138 and #1139, which this is branched off; the diff against
maincollapses once those merge.
AL-576, the add-guardrail palette, third member of the shared guardrails family in
packages/apollo-react/src/canvas/components/Guardrails/. Branched offfeat/apollo-react-guardrail-definitions-layer(#1139) rather than stacked on #1140, so thestreams stay a tree: stack only on what you import from.
Rebased onto #1139's current tip on 2026-09-14, which moved this branch from 25 commits over
mainto 12: #1107 and #1138 have each been squashed to one commit, and the shared chip hasmoved down into #1139 (see below).
What ships
guardrail-palette.tsxGuardrailPalette,GuardrailPalettePropsguardrail-palette-utils.tsgroupGuardrailsForPalette,getGuardrailPaletteItemId. Pure functions, callable outside React (the module graph is not React-free: it reachesdefinitions-copythroughisByoGuardrailDefinition).palette-types.tsGuardrailPaletteDefinition,GuardrailPaletteGroupcomponents/guardrail-palette-item.tsxGuardrailPaletteItem(not exported fromindex.ts: the narrow surface is the palette)i18n.ts(extended)GuardrailPaletteLabels,GUARDRAIL_PALETTE_EN_LABELS,GUARDRAIL_PALETTE_EN_MESSAGES,resolveGuardrailPaletteLabels,useGuardrailPaletteLabelscomponents/guardrail-status-chip.tsxGuardrailStatusChip(now on #1139, inherited here and by #1140)The shared chip has moved to #1139
GuardrailStatusChip, its test, and theGUARDRAIL_CHIP_GEOMETRYconstant inguardrail-chip.tsxare no longer in this PR. They used to be the first two commits here andbyte-identical copies of them were the same two commits on #1140, kept in sync by hand with one
cherry-pick per edit. Both commits are now on #1139, where all three leaves and AL-578 inherit
them from one place. They dropped out of this branch on rebase, mostly by patch-id.
For reviewers: the chip is a
<span>composed from wind's exportedbadgeVariantsrather thanthe
Badgecomponent, which renders a<div>, because the palette entry places these chipsinside its
<button>and flow content is invalid there. Review it on #1139.Decisions worth arguing with
Select. Flow's picker is a windSelect; Agents' is a scrollablesidebar list. The list is the more capable of the two: it composes into a dialog, an inline
overlay or a sidebar, and it shows the descriptions without a popover. Flow's own
SelectContentalready fights the primitive to get there (multi-line items,disableditems, sticky group headers,
h-auto min-h-9on the trigger, two!importantviewportoverrides), which is the evidence. QA-visible for Flow: the type dropdown becomes a
one-step list.
Commandeither, which is the other obvious candidate. cmdk is a combobox:its items are
role="option"witharia-selected, it expects a filter input, and thepattern models a selection. Palette entries are one-shot actions. More decisively, cmdk's
keyboard navigation skips disabled items by construction: its item query is
[cmdk-item]:not([aria-disabled="true"]), checked in the installed dist. The one thing thispalette insists on is that a keyboard user reaches the
Unauthorisedentry to read thechip saying why it cannot be picked, and
Commandmakes that impossible. Neither host'scurrent picker is cmdk either.
divwithrole="listitem",tabIndex={0}and its own Enter/Space handler, which puts a list item outside a list andreimplements a button. An
Unauthorisedentry isaria-disabled, notdisabled, so itstays focusable and a keyboard user reaches the chip that says why.
so Tab enters the palette and Tab leaves it rather than walking a user through a
create-custom entry, six UiPath validators and every BYO group on the way past.
ArrowDown / ArrowUp step between entries across group boundaries, Home and End jump to the
first and last, the tab stop follows focus so leaving and re-entering comes back where the
user was, and movement clamps at the ends rather than wrapping.
aria-disabledentries areincluded in that order on purpose. Two tests cover it, one for the keys and one asserting
the unauthorized entry is still reached.
<output>, for its implicitrole="status": a polite live region,and the one native element that carries it without bringing styling of its own.
Unauthorisedis not choosable. Flow's behaviour. QA-visible for Agents: today thatentry opens a builder that then refuses to save.
GuardrailPaletteDefinition(the eight fields the palettereads), for the same reason the list keeps its own definition shape.
EnrichedGuardrailDefinitionsatisfies it andonSelectOotbhands the host's own objectback,
parametersand all, so there is nothing to look up again. Agents'OutOfTheBoxGuardrailDefinition, whosedescriptionis optional, also fits.and an inline overlay that renders the chosen builder underneath, Agents takes over the
sidebar with a back button and its own create affordance. Composing either is a handful of
lines around this component: the
InAHostSidebarstory is 24,InADialog37.onCreateCustomis opt-in by presence. Flow offers custom guardrails forscope === 'Tool'only, so it passes the prop conditionally; Agents keeps the affordance in its ownheader, so it omits the prop and gets no duplicate entry.
groupGuardrailsForPalette,which returns one empty group. That is what makes an empty state reachable at all: Flow
guards its own empty message on
groups.length === 0, which its implementation can neverreturn, so the message is dead code there.
localeCompare()with no locale argument, the same as both products today. Recorded in the JSDoc rather than
changed, because changing it is a product decision.
mergeLabels: with a third label set, the merge loop is factored out. The helper isbyte-identical to the one feat(apollo-react): guardrail list section [AL-575] #1140 introduces for the same reason, so the two merge clean.
Review fixes (
7f07afd7)tscerror in this PR is gone.renderPalettecast itsPartialpropsonto the spread, so TypeScript reported that the helper's own two defaults are always
overwritten (two TS2783). The helper now builds a typed
GuardrailPalettePropsobject andspreads that. CI cannot see this class of error at all, since it does not typecheck tests or
stories; the check used here is in Verification below.
chip idiom and its own:
hover:bg-accent hover:text-accent-foreground(windButtonghost,CommandItemselected,DropdownMenuItemfocus) instead ofhover:bg-muted, and gated onthe enabled branch so the
hover:bg-transparentcounter-rule is gone;opacity-50insteadof
opacity-60(Button,CommandItem);ring-offset-backgroundplusfocus-visible:ring-offset-2to completeButton's focus ring; the leading icon ontext-muted-foreground, which the rest of the family uses ten times against this onetext-foreground-muted.onClickis omitted fromGuardrailPaletteItemPropsalongsideonSelect. The disabledguard is
onClick={disabled ? undefined : onSelect}followed by{...props}, so a callerspreading its own
onClickcould silently reinstate a handler past it.aria-labelledbypointing at the visible header(through
useId) instead of repeating the header text inaria-label.through the entries" that a reviewer would otherwise raise against Flow's
Select.<output>line and the token choices.Second-pass review fixes (
08e76fc7)Copilot's second pass caught one real bug and two smaller things; all are fixed.
family in one.
handleKeyDownlocated the focused entry withdocument.activeElement, whichretargets to the shadow host there, so the lookup never matched and Arrow/Home/End did
nothing. It now reads the entry off the keyboard event's target, which React delivers
un-retargeted for an event raised in its own tree. A new test renders into
host.attachShadow({ mode: 'open' })and arrows down; it fails against the previous handler,which is how the fix was verified rather than assumed.
<span>: aBadge<div>inside the entry's<button>is invalidcontent. That fix now lives on feat(apollo-react): shared guardrail definitions layer and useGuardrailDefinitions [AL-574] #1139 with the rest of the chip.
ru. It is deliberately empty, which is not licence to keep astale id in it; the coverage check still leaves it out.
a React-free module. The functions take no hooks and no context, but the import graph reaches
React through
definitions-enrichanddefinitions-copy, so the stronger claim was wrong.Two further suggestions are not taken, deliberately: tracking the roving index by entry id
rather than by position (the only window it closes is a catalog that reorders while focus is
inside the palette and the user tabs out and back), and namespacing the group keys against a
real BYO folder literally named
__uipath__. Both are theoretical, and the second is a changeto an exported contract (
GuardrailPaletteGroup.key) for no observed failure.Strings
Nine
guardrails.palette.*ids, one builder function holding every_({ id, message })call(the
definitions-copy.tsidiom). All nine harvested from the products' own catalogs: eightfrom Flow's
addGuardrailPalette_*i18next keys,list-aria-labelfrom Agents'guardrails.palette.list_aria_labellingui id, with English parity asserted against thesource product before writing. 9 of 9 in each of the 12 translated locales;
ruleftempty per this catalog's convention.
i18n.test.tsasserts catalog parity both ways plusfull translation coverage, which is what
lingui extractwould otherwise do (src/canvasuses no macros, so extraction does not feed this catalog).
The last commit swaps this file's hand-rolled copy of those three scans for the shared ones in
__fixtures__/catalog-coverage, added on #1139. Every component was carrying the same fortylines; the assertions are unchanged.
One loc review item: German takes "Leitplanke" from Flow for eight ids and "Leitlinien" from
Agents for the ninth, because the two products picked different words for "guardrail".
Verification
Re-run in full after the 2026-09-14 rebase.
tsc --noEmitclean, biome clean on every touched file.noEmitandincludelimited to this folder reports no errors in this PR's files. Thetwelve it does report are feat(apollo-react): guardrails component family under canvas #1138's own, present on its head: seven TS2532 in
guardrail-builder.test.tsx, two inform-schema-builder.test.ts, and one each inguardrail-builder.stories.tsx,guardrail-form-layout.test.tsxandguardrail-validator-form.test.tsx.None of the 9 failures are this PR's. Eight are the known pre-existing
localStoragefailures in
canvas/utils/Storage.test.tsandcanvas/hooks/useStorageState.test.ts(Node 24ships an experimental
localStorageglobal that isundefinedwithout--localstorage-fileand shadows happy-dom's). The ninth is #1138's own
guardrail-validator-form.test.tsx > localization > resolves the resolver validation messages from the catalog, not wind English, which fails identically on #1138's own catalogs; see #1139for the diagnosis.
Open question
Should the palette also render
FeatureDisabled/Disableddefinitions as disabled entries?It does not today: both products filter them out before rendering, and
Unauthorisedis theonly non-
Availablestatus that reaches a correctly filtered palette by design. Say the wordand the status-to-chip mapping generalizes.
Title-less alert alignment (
e7e12307). Wind'sAlertDescriptionis unconditionallymt-1, which assumes anAlertTitlesits above it, whilealertVariantspins the icon at[&>svg]:top-3.5. With no title the text lands 4px below the icon, visible in the Load Failed story. Fixed withmt-0at each call site, the same hunk as #1161 and #1140 so the branches merge without a conflict. The real fix is one line in wind'salertVariants,[&>[data-slot=alert-description]:first-child]:mt-0, which would let all three workarounds be deleted; that is a separate PR.