Skip to content

feat(apollo-react): add-guardrail palette [AL-576] - #1147

Draft
andreizdrali-uipath wants to merge 14 commits into
mainfrom
feat/apollo-react-guardrail-palette
Draft

andreizdrali-uipath wants to merge 14 commits into
mainfrom
feat/apollo-react-guardrail-palette

Conversation

@andreizdrali-uipath

@andreizdrali-uipath andreizdrali-uipath commented Sep 10, 2026

Copy link
Copy Markdown

Review only the last five commits (41abd5f5 the palette, 7f07afd7 the first round of
review fixes, 08e76fc7 the second pass, 878a2934 the shared catalog scans, e7e12307 the
title-less alert offset). The eight
below them are #1107, #1138 and #1139, which this is branched off; the diff against main
collapses 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 off
feat/apollo-react-guardrail-definitions-layer (#1139) rather than stacked on #1140, so the
streams 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
main to 12: #1107 and #1138 have each been squashed to one commit, and the shared chip has
moved down into #1139 (see below).

The Jira description of AL-576 is stale. It still says apollo-wind and component-scoped
locales, both superseded on 2026-09-09. Read it as apollo-react family, strings as lingui ids
in the shared canvas catalog.

The dev preview pair is stale. @uipath/apollo-react@6.43.0-pr1147.95dda0a /
@uipath/apollo-wind@2.48.0-pr1147.95dda0a was published before any of the rebases and still
carries #1107's superseded MetadataForm controlled-host seam. Until now nothing could
republish it: every branch under #1107 conflicted with main in
packages/apollo-wind/src/components/ui/select.tsx, so GitHub built no test-merge ref and
fired no pull_request workflows at all. That conflict is gone as of 2026-09-14 and all
four PRs in the stack merge cleanly, so pr-checks and dev-publish should run and a fresh
pair should appear.

What ships

File Exports
guardrail-palette.tsx GuardrailPalette, GuardrailPaletteProps
guardrail-palette-utils.ts groupGuardrailsForPalette, getGuardrailPaletteItemId. Pure functions, callable outside React (the module graph is not React-free: it reaches definitions-copy through isByoGuardrailDefinition).
palette-types.ts GuardrailPaletteDefinition, GuardrailPaletteGroup
components/guardrail-palette-item.tsx GuardrailPaletteItem (not exported from index.ts: the narrow surface is the palette)
i18n.ts (extended) GuardrailPaletteLabels, GUARDRAIL_PALETTE_EN_LABELS, GUARDRAIL_PALETTE_EN_MESSAGES, resolveGuardrailPaletteLabels, useGuardrailPaletteLabels
components/guardrail-status-chip.tsx GuardrailStatusChip (now on #1139, inherited here and by #1140)

The shared chip has moved to #1139

GuardrailStatusChip, its test, and the GUARDRAIL_CHIP_GEOMETRY constant in
guardrail-chip.tsx are no longer in this PR. They used to be the first two commits here and
byte-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 exported badgeVariants rather than
the Badge component, which renders a <div>, because the palette entry places these chips
inside its <button> and flow content is invalid there. Review it on #1139.

Decisions worth arguing with

  • A list, not a Select. Flow's picker is a wind Select; Agents' is a scrollable
    sidebar 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
    SelectContent already fights the primitive to get there (multi-line items, disabled
    items, sticky group headers, h-auto min-h-9 on the trigger, two !important viewport
    overrides), which is the evidence. QA-visible for Flow: the type dropdown becomes a
    one-step list.
  • Not wind's Command either, which is the other obvious candidate. cmdk is a combobox:
    its items are role="option" with aria-selected, it expects a filter input, and the
    pattern 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 this
    palette insists on is that a keyboard user reaches the Unauthorised entry to read the
    chip saying why it cannot be picked, and Command makes that impossible. Neither host's
    current picker is cmdk either.
  • Entries are real buttons. Agents' entry is a div with role="listitem",
    tabIndex={0} and its own Enter/Space handler, which puts a list item outside a list and
    reimplements a button. An Unauthorised entry is aria-disabled, not disabled, so it
    stays focusable and a keyboard user reaches the chip that says why.
  • The palette is one tab stop, with roving focus inside it. Exactly one entry is tabbable,
    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-disabled entries are
    included in that order on purpose. Two tests cover it, one for the keys and one asserting
    the unauthorized entry is still reached.
  • The loading line is an <output>, for its implicit role="status": a polite live region,
    and the one native element that carries it without bringing styling of its own.
  • Unauthorised is not choosable. Flow's behaviour. QA-visible for Agents: today that
    entry opens a builder that then refuses to save.
  • Definitions are generic over GuardrailPaletteDefinition (the eight fields the palette
    reads), for the same reason the list keeps its own definition shape.
    EnrichedGuardrailDefinition satisfies it and onSelectOotb hands the host's own object
    back, parameters and all, so there is nothing to look up again. Agents'
    OutOfTheBoxGuardrailDefinition, whose description is optional, also fits.
  • Only the picker ships. No dialog wrapper, not even a thin one: Flow opens a 500px dialog
    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 InAHostSidebar story is 24, InADialog 37.
  • onCreateCustom is opt-in by presence. Flow offers custom guardrails for scope === 'Tool' only, so it passes the prop conditionally; Agents keeps the affordance in its own
    header, so it omits the prop and gets no duplicate entry.
  • An empty catalog produces no groups, unlike both products' 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 never
    return, so the message is dead code there.
  • Group and entry ordering follows the runtime locale. Both sorts are 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 is
    byte-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)

  • The only strict-tsc error in this PR is gone. renderPalette cast its Partial props
    onto the spread, so TypeScript reported that the helper's own two defaults are always
    overwritten (two TS2783). The helper now builds a typed GuardrailPaletteProps object and
    spreads 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.
  • The entry moved onto wind's own interactive-item idiom, away from a mix of this family's
    chip idiom and its own: hover:bg-accent hover:text-accent-foreground (wind Button ghost,
    CommandItem selected, DropdownMenuItem focus) instead of hover:bg-muted, and gated on
    the enabled branch so the hover:bg-transparent counter-rule is gone; opacity-50 instead
    of opacity-60 (Button, CommandItem); ring-offset-background plus
    focus-visible:ring-offset-2 to complete Button's focus ring; the leading icon on
    text-muted-foreground, which the rest of the family uses ten times against this one
    text-foreground-muted.
  • onClick is omitted from GuardrailPaletteItemProps alongside onSelect. The disabled
    guard is onClick={disabled ? undefined : onSelect} followed by {...props}, so a caller
    spreading its own onClick could silently reinstate a handler past it.
  • Nested groups name themselves with aria-labelledby pointing at the visible header
    (through useId) instead of repeating the header text in aria-label.
  • Roving focus added (the bullet above), which is the answer to "Tab is the only way
    through the entries" that a reviewer would otherwise raise against Flow's Select.
  • README gains the keyboard model, the <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.

  • The roving focus was a no-op inside a shadow root, i.e. in Agents, which renders the
    family in one. handleKeyDown located the focused entry with document.activeElement, which
    retargets 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.
  • The status chip is a <span>: a Badge <div> inside the entry's <button> is invalid
    content. That fix now lives on feat(apollo-react): shared guardrail definitions layer and useGuardrailDefinitions [AL-574] #1139 with the rest of the chip.
  • The i18n orphan scan covers ru. It is deliberately empty, which is not licence to keep a
    stale id in it; the coverage check still leaves it out.
  • The palette-utils JSDoc says "pure functions, callable outside React" rather than claiming
    a React-free module. The functions take no hooks and no context, but the import graph reaches
    React through definitions-enrich and definitions-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 change
to 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.ts idiom). All nine harvested from the products' own catalogs: eight
from Flow's addGuardrailPalette_* i18next keys, list-aria-label from Agents'
guardrails.palette.list_aria_label lingui id, with English parity asserted against the
source product before writing. 9 of 9 in each of the 12 translated locales; ru left
empty per this catalog's convention. i18n.test.ts asserts catalog parity both ways plus
full translation coverage, which is what lingui extract would otherwise do (src/canvas
uses 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 forty
lines; 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.

  • Guardrails suite 379 passing, 1 failing, jest-axe on both rendering suites.
  • Full package suite 3020 passing, 9 failing.
  • tsc --noEmit clean, biome clean on every touched file.
  • Tests and stories typechecked separately, which CI does not do: a throwaway tsconfig with
    noEmit and include limited to this folder reports no errors in this PR's files. The
    twelve 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 in form-schema-builder.test.ts, and one each in
    guardrail-builder.stories.tsx, guardrail-form-layout.test.tsx and
    guardrail-validator-form.test.tsx.

None of the 9 failures are this PR's. Eight are the known pre-existing localStorage
failures in canvas/utils/Storage.test.ts and canvas/hooks/useStorageState.test.ts (Node 24
ships an experimental localStorage global that is undefined without --localstorage-file
and 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 #1139
for the diagnosis.

Open question

Should the palette also render FeatureDisabled / Disabled definitions as disabled entries?
It does not today: both products filter them out before rendering, and Unauthorised is the
only non-Available status that reaches a correctly filtered palette by design. Say the word
and the status-to-chip mapping generalizes.

Rebased 2026-09-14 onto #1139's new head, which sits on #1138's squashed 9fa10765. The
two chip commits left this branch for #1139 and the remaining four have new shas (the ones
named in this body are already repointed). The select.tsx conflict that kept this stack
CONFLICTING is resolved upstream, so pull_request workflows should fire here for the first
time.

Title-less alert alignment (e7e12307). Wind's AlertDescription is unconditionally mt-1, which assumes an AlertTitle sits above it, while alertVariants pins the icon at [&>svg]:top-3.5. With no title the text lands 4px below the icon, visible in the Load Failed story. Fixed with mt-0 at 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's alertVariants, [&>[data-slot=alert-description]:first-child]:mt-0, which would let all three workarounds be deleted; that is a separate PR.

Copilot AI lite review requested due to automatic review settings September 10, 2026 11:13
@andreizdrali-uipath andreizdrali-uipath added the dev-packages Adds dev package publishing on pushes to this PR label Sep 10, 2026
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Apollo Coded App preview deployments are ready.

Project Status Preview Updated (PT)
apollo-design Ready Preview · Logs Sep 14, 2026, 08:27:18 AM
apollo-docs Ready Preview · Logs Sep 14, 2026, 08:27:18 AM
apollo-landing Ready Preview · Logs Sep 14, 2026, 08:27:18 AM
apollo-vertex Ready Preview · Logs Sep 14, 2026, 08:27:18 AM

@github-actions

Copy link
Copy Markdown
Contributor

Dependency License Review

  • 1937 package(s) scanned
  • ✅ No license issues found
  • ⚠️ 2 package(s) excluded (see details below)
License distribution
License Packages
MIT 1708
ISC 88
Apache-2.0 55
BSD-3-Clause 27
BSD-2-Clause 23
BlueOak-1.0.0 8
MPL-2.0 4
MIT-0 3
CC0-1.0 3
MIT OR Apache-2.0 2
(MIT OR Apache-2.0) 2
Unlicense 2
LGPL-3.0-or-later 1
Python-2.0 1
CC-BY-4.0 1
(MPL-2.0 OR Apache-2.0) 1
Unknown 1
Artistic-2.0 1
(WTFPL OR MIT) 1
(BSD-2-Clause OR MIT OR Apache-2.0) 1
CC-BY-3.0 1
0BSD 1
(MIT OR CC0-1.0) 1
MIT AND ISC 1
Excluded packages
Package Version License Reason
@img/sharp-libvips-linux-x64 1.3.2 LGPL-3.0-or-later LGPL pre-built binary, not linked
khroma 2.1.0 Unknown MIT per GitHub repo, missing license field in package.json

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

📦 Dev Packages

Package Status Updated (PT)
@uipath/apollo-react@6.45.1-pr1147.e24e87f 🟢 Published Sep 14, 2026, 08:26:22 AM
@uipath/apollo-wind@2.49.0-pr1147.e24e87f 🟢 Published Sep 14, 2026, 08:25:11 AM

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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, error is truthy but isEmpty is 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.key is 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 using group.key alone.
    packages/apollo-wind/src/components/forms/field-renderer.tsx:474
  • MultiSelect deliberately uses its id to 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.

Comment on lines +24 to +26
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__',
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

📊 Coverage + size by package

Per-package coverage and bundle size on this PR. New-line coverage = of the source lines this PR adds or changes, the % hit by tests.

Package Coverage New-line coverage Packed (gzip) Unpacked vs main
@uipath/apollo-core 42.01 MB 50.12 MB ±0
@uipath/apollo-react 45.3% 85.7% (790/922) 7.74 MB 29.91 MB +180.6 KB
@uipath/apollo-ui-icons 2.85 MB 6.91 MB ±0
@uipath/apollo-wind 66.8% 95.0% (114/120) 460.4 KB 2.91 MB +8.9 KB
@uipath/ap-chat 85.8% 43.95 MB 56.84 MB +27.6 KB

"Coverage" is each package's own coverage.include scope (e.g. apollo-core instruments only scripts/). "Packed"/"Unpacked" come from npm pack --dry-run and only cover built packages — "—" means not measured this run (package not affected / not built). "vs main" is the packed (gzipped) delta against the last successful main build (the package-sizes artifact from the Release workflow); "—" there means no main baseline was available this run. The baseline is main's latest build, not this PR's exact merge-base, so it includes any drift since the branch diverged. Packages with no vitest config are omitted.

@andreizdrali-uipath

Copy link
Copy Markdown
Author

Published previews, both green:

PR apollo-react apollo-wind
this PR (#1147) 6.43.0-pr1147.95dda0a 2.48.0-pr1147.95dda0a
preview build (#1148), palette + list 6.43.0-pr1148.f612ad1 2.48.0-pr1148.f612ad1

Host sessions want #1148's pair: this PR's preview has no list, and #1140's has no palette.
Note the minor bump from the #1140 pair (main moved to 6.43.0 / 2.48.0).

CI: Test Coverage, Typecheck, Lint, Format and Build green. The two red Audit * Dependencies
checks are the repo's Dependabot backlog on main, identical on #1138 and #1140; no dependency
is added here and Detect Lockfile Changes is green.

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Storybook visual diff

⚠️ Visual changes detected: 15 changed, 41 added (of 1071 compared, 1015 unchanged). View report

Baseline 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

Copilot AI review requested due to automatic review settings September 11, 2026 08:34
@andreizdrali-uipath
andreizdrali-uipath force-pushed the feat/apollo-react-guardrail-palette branch from 27643cd to 03bb89c Compare September 11, 2026 08:34
andreizdrali-uipath added a commit that referenced this pull request Sep 11, 2026
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>
Comment thread packages/apollo-wind/src/components/forms/metadata-form.test.tsx Fixed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

  • GuardrailStatusChip renders the wind Badge, 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 React validateDOMNesting warnings) 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 folderPath or 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 isByoGuardrailDefinition as a runtime value from definitions-enrich pulls in its GUARDRAIL_COPY_EN dependency and therefore definitions-copy.ts, which imports React and useSafeLingui. 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

Copilot AI review requested due to automatic review settings September 11, 2026 09:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 chips slot is inserted inside the palette item's <button>, but GuardrailStatusChip delegates to wind Badge, 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

  • Badge renders a <div>, so this status chip becomes a <div> inside the <button> emitted by GuardrailPaletteItem (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 folderPath or 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
  • activeEntry is 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 receives tabIndex=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

andreizdrali-uipath added a commit that referenced this pull request Sep 11, 2026
… 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.
Copilot AI review requested due to automatic review settings September 11, 2026 10:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 isByoGuardrailDefinition from definitions-enrich pulls in definitions-copy's runtime useMemo/useSafeLingui dependency. A non-React caller of groupGuardrailsForPalette therefore 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

  • buildFieldValidation is attached to every guardrail field here, but GuardrailValidatorForm runs MetadataForm with schemaMode: 'onChange' and this version has no disableValidation prop. Required/range resolver errors consequently appear while editing even though the documented guardrail contract is validation-free and host errors are 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 onCreateCustom is added while the second definition is focused, activeEntry stays 1; the previously focused definition shifts to index 2 and gets tabIndex={-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 })) {
andreizdrali-uipath added a commit that referenced this pull request Sep 11, 2026
… 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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), ApolloMessageRenderer therefore changes type and React unmounts/remounts the renderer subtree, resetting ApToolCall's expanded state and nested trace state. Memoize the renderer list by locale before 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

apetraru-uipath and others added 2 commits September 14, 2026 12:19
…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 AlertDescription mt-1 places its text below the icon at top-3.5. Add the mt-0 override 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's mt-1 even though the icon is positioned at top-3.5, leaving the mixed-scope message lower than the icon. Add the mt-0 override here, as in GuardrailStatusBanner, to keep the alert contents aligned.
      <AlertDescription>

packages/apollo-react/src/canvas/components/Guardrails/guardrail-builder.tsx:309

  • This alert has no AlertTitle, but AlertDescription defaults to mt-1 while the icon is positioned at top-3.5. The usage-note text is therefore visibly offset below its icon, so apply the same mt-0 override used by GuardrailStatusBanner.
    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 ApolloMessageRenderer changes type whenever AutopilotChatMessageContent re-renders. React then unmounts and remounts ApToolCall/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

andreizdrali-uipath added a commit that referenced this pull request Sep 14, 2026
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>
andreizdrali-uipath added a commit that referenced this pull request Sep 14, 2026
…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>
andreizdrali-uipath and others added 12 commits September 14, 2026 18:09
…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>
andreizdrali-uipath added a commit that referenced this pull request Sep 14, 2026
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>
Copilot AI review requested due to automatic review settings September 14, 2026 15:17
@andreizdrali-uipath
andreizdrali-uipath force-pushed the feat/apollo-react-guardrail-palette branch from e7e1230 to 596e3e3 Compare September 14, 2026 15:17
andreizdrali-uipath added a commit that referenced this pull request Sep 14, 2026
…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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's mt-1, leaving its message lower than the info icon. Add the mt-0 override 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 no id and no aria-label; the placeholder is not an accessible name. Add a stable htmlFor and matching id to both fallback inputs (or expose an equivalent aria-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 FormFieldError is not referenced by aria-describedby (and Input'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 default AlertDescription margin offsets the first paragraph below the icon. Add the title-less mt-0 override, as in GuardrailStatusBanner, 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

  • InfoTooltip renders 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, as FormFieldLabel does, while retaining it inside the header div for asTextHeader.
      {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, but AlertDescription defaults to mt-1, so the usage-note text is rendered 4px below the icon. Apply the same title-less override used by GuardrailStatusBanner so the note is vertically aligned.
  • Files reviewed: 108/109 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dev-packages Adds dev package publishing on pushes to this PR pkg:apollo-react pkg:apollo-wind size:XXL 1,000+ changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants