feat(apollo-react): guardrail remove confirmation dialog [AL-577] - #1149
feat(apollo-react): guardrail remove confirmation dialog [AL-577]#1149andreizdrali-uipath wants to merge 25 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
The dialog list keys can collide and must be made unique before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds a shared Apollo React guardrail removal confirmation dialog with structured impact messaging, localization, controlled lifecycle, and accessibility support.
Changes:
- Added the dialog, stories, tests, and public exports.
- Added localized labels and catalog parity coverage.
- Documented guardrail removal behavior and integration contracts.
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 |
Reviewed; no final comment. |
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/index.ts |
Reviewed; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/i18n.test.ts |
Reviewed catalog parity coverage; 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-remove-dialog.tsx |
Moderate (2 votes): raw list keys can collide across item kinds or repeated values; use unique keys. |
packages/apollo-react/src/canvas/components/Guardrails/guardrail-remove-dialog.test.tsx |
Reviewed dialog behavior and accessibility coverage; no final comment. |
packages/apollo-react/src/canvas/components/Guardrails/guardrail-remove-dialog.stories.tsx |
Reviewed dialog stories; 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-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-scope-selector.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-utils.test.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__/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
- Files reviewed: 85/86 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
📊 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 |
There was a problem hiding this comment.
🔵 Needs a closer look
Three moderate findings remain regarding impact ordering, list-key uniqueness, and the ru catalog orphan scan.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
packages/apollo-react/src/canvas/components/Guardrails/guardrail-remove-dialog.tsx:85
ImpactListrenderstoolNamesbeforescopes, but the dialog contract requires scopes to precede tools. WithaffectedToolNames={['Send email']}andaffectedScopes={['Llm']}, this still producesSend email, LLM callsinstead of the promised scope-first order; render the scope items before the tool items and update the order assertions.
packages/apollo-react/src/canvas/components/Guardrails/guardrail-remove-dialog.tsx:89
- These keys come from two independent arrays rendered as siblings in the same
<ul>. A tool namedAgent,Llm, orToolcan therefore collide with a scope key (and repeated values collide within either array), producing duplicate-key warnings and potentially stale item reconciliation when the impact changes. Prefix the keys by source and include an index, or otherwise generate unique keys for both maps.
packages/apollo-react/src/canvas/components/Guardrails/i18n.test.ts:107 - The orphan-id check claims to cover every catalog, but this locale list omits
ru, which is still a checked-in catalog. A staleguardrails.remove-dialog.*entry inru.jsonwould pass unnoticed; includeruin this orphan scan while keeping it out of the translated-coverage check because it intentionally falls back to English.
- Files reviewed: 85/86 changed files
- Comments generated: 0 new
- Review effort level: Lite
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 11, 2026, 06:35:33 AM |
7db5698 to
d816880
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
The dialog currently renders tool names before scopes; update the component and its test before approval.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
packages/apollo-react/src/canvas/components/Guardrails/guardrail-remove-dialog.tsx:90
- The dialog contract says scopes should appear before tool names in the impact list, but this renders
toolNamesfirst andscopessecond. Once the adapters pass the corrected arrays, users will still see the list in the wrong order (andguardrail-remove-dialog.test.tsxcurrently locks in that order); render the scopes before the tools and update the expectation.
- Files reviewed: 102/103 changed files
- Comments generated: 0 new
- Review effort level: Lite
… field Generic forms-engine enablers extracted from the guardrails work (apollo-ui#1107 review): controlled values/onValuesChange/errors/disableValidation/container props on MetadataForm (MetadataFormProps now exported), a string-list field type with tooltip/textarea/multiselect metadata additions, label association and custom-component ref handling in the field renderer, and a useWatch re-export so cross-package custom fields share the RHF context. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ling InfoTooltip moves out of the guardrails prototype into components/ui with its a11y test; select and textarea get aria-invalid error styling; the root barrel exposes the new forms/ui surface. The guardrails domain family itself moves to apollo-react (canvas) per the #1107 review decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up. StringListField rendered the FormFieldError text but never marked the rows invalid, so the aria-invalid styling this PR adds to Textarea never activated and assistive tech got no invalid signal for the inputs. Every row now carries it — the error belongs to the list as a whole. Also adds the InfoTooltip storybook entry requested in review (default, label-adjacent, rich and long content), documenting that an ancestor TooltipProvider is required and that schema-driven forms get the trigger from the field `tooltip` metadata. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nts at Review follow-up: the label-adjacent story set htmlFor="blocked-phrases" with no such control, which is a dangling association and a misleading a11y example in the one story specifically about label placement. It now renders the Textarea the label names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review follow-up: rather than a near-duplicate label component, FormFieldLabel now takes `tooltip`/`tooltipAriaLabel` and renders the trigger after the required indicator. That removes the reason the field-renderer helper existed (it composed RequiredIndicator by hand purely to control that ordering), so the helper is gone and the 11 call sites use FormFieldLabel with its `required` prop directly. The tooltip is now available to every FormFieldLabel consumer, not just schema-driven forms; the InfoTooltip story shows that path as the idiomatic one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review follow-up, eight findings: - container='div' rendered schema submit actions as type="submit" with no owning <form>, so a click would submit whatever ancestor form the host embedded the fields in — the exact hazard div mode exists to avoid. Submit actions are now plain buttons wired to the form's own submit handler. - The tooltip trigger is a <button>, which <label>'s content model forbids as a descendant; it now renders beside the label in an inline wrapper, keeping the ref, htmlFor and styling on the label element. - text/email and number inputs never received aria-invalid, unlike the textarea/select paths, so their invalid styling and ARIA state never applied. - multiselect and file-upload had no label association: both now get the field id with htmlFor on the label (file upload also takes the field label as its dropzone name). - string-list never forwarded the controller's onBlur, so blur-mode validation and touched state never fired for it. - Textarea's new aria-invalid border is invisible in the future theme, which renders it borderless; it now carries the same invalid ring as Input. - The InfoTooltip story used the React UMD namespace without importing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second review pass, six findings — three of them gaps left by the previous round: - container='div' only neutralised the single-page submit button. Wizard and tabbed schemas still rendered type="submit", so the ancestor-form hazard survived for stepped forms; both step renderers now take the same handler. - file upload got the id but not htmlFor, so the label still was not associated with the control; it also never received aria-invalid, which FileUpload supports. - Select carried the same borderless-future-theme problem just fixed on Textarea, so its aria-invalid styling was invisible there. - A required multiselect or string-list accepted an empty array: zod treats [] as present, and the required branch only enforced non-empty for strings. Pre-existing for multiselect, and newly reachable for string-list. - Story copy used a spaced em dash, which the repo's Storybook rule forbids. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…olated Third review pass: - A schema with tooltip metadata crashed a bare MetadataForm: Radix throws without an ancestor TooltipProvider, and nothing mounted one — the story had to wrap manually, which was the tell. The form now supplies its own provider, but only for schemas that use tooltips, since a provider carries delay settings and an unconditional one would override a host's configuration. The story's manual wrapper is gone. - container='div' removed the <form> but not implicit submission: Enter in a single-line input still submits the host's ancestor form, contradicting the documented contract. Enter is now swallowed for inputs, leaving textareas (newlines) and buttons (activation) alone. - schema-serializer dropped every field property this PR added — tooltip, tooltipAriaLabel, minRows, maxLength, maxItems, emptyMessage, searchPlaceholder, addItemLabel, removeItemAriaLabel — so a round-trip through serializeSchema silently lost them. - The checkbox branch ignored tooltip metadata that every other field type honours. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fourth review pass, and this one hit the controlled-host seam itself. Initialization resets the form from schema.initialData asynchronously, so the reset lands after the values sync effect has already run. The prop reference is unchanged, so nothing re-applies it: a controlled host silently ended up displaying and submitting the schema's data instead of its own. Verified by probe before and after the fix. The sync now also depends on isInitialized, which is free when nothing differs thanks to the per-field deep-equal guard. StringListField is exported standalone and renders the Radix info tooltip when field.tooltip is set, so its contract now states the TooltipProvider requirement — MetadataForm keeps handling it for schema-driven usage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up review (BenGSchulz): the controlled-host seam routed around three features the schema contract already declared but never implemented. Repairing them serves every MetadataForm consumer, not just guardrails. - ValidationConfig.custom was typed, documented as a jsep expression and serialized, but the converter never read it. Now enforced. The expression is parsed once at schema-build time, because the evaluator returns false for a malformed expression exactly as for a legitimately failing one — a schema typo would otherwise make a field permanently unsubmittable, so an unparseable expression is simply not enforced. - plugin.onValueChange sat behind a mount-lifetime gate, so a plugin's first keystroke could be swallowed. It is now suppressed only while initialization's reset writes schema data. - Custom fields validated as z.any(), where required and minItems are no-ops. They can now declare a valueType, so the ordinary metadata constraints reach custom components. Also: one emptiness predicate shared by the resolver and the conditional- required superRefine, which previously disagreed — the resolver path missed whitespace-only strings. And the container='div' actions gate is gone; suppressing the action row is the schema's job via actions: [], which FormActions already honours. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up review (BenGSchulz): keep MetadataForm's contract as it was. Values live in react-hook-form, validation lives in the schema, and hosts interact through schema, plugins and remount. Removes `values`, `onValuesChange`, `errors`, `disableValidation` and `components`. They existed to route around three features the schema already declared but never implemented — all repaired in the preceding commit — so the seam bought nothing the intended path could not do. A second source of truth for values also means two things can disagree about what the user typed, and every consumer reimplements the reconciliation. `FormPlugin.components` stays honoured from the first render, which is what the `components` prop was compensating for. Tests and the string-list story move onto the plugin path, validating through `minItems` rather than a host-side reimplementation of the same rule, and forms/README.md now states the ownership contract once, naming NodePropertyPanel and ValidationPlugin as reference hosts. BREAKING CHANGE: MetadataForm no longer accepts `values`, `onValuesChange`, `errors`, `disableValidation` or `components`. Use schema validation, a FormPlugin (`onValueChange` / `context.form.setValue` / `setError`) and remount instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Awaiting a plugin onFormInit that returns void still defers setIsInitialized into a microtask, so it lands outside React's act() scope and every host test that renders synchronously emits an act warning — enough to fail a suite that treats console output as an error. Only promise-returning hooks are awaited now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review follow-up (BenGSchulz + Copilot). Two of these correct claims I made in
earlier replies rather than defects I found myself.
- Required strings accepted whitespace. I had said the resolver and the
conditional-required superRefine now share one emptiness predicate; they did
not. `z.string().min(1).safeParse(' ')` succeeds, so the same
`validation: { required: true }` meant different things depending on whether
the field happened to carry rules. The static branch now goes through
`isEmptyFieldValue` as a `.refine`, not `.trim().min(1)`, which would mutate
the submitted value.
- The jsep guard checked the wrong thing. It only caught expressions that fail
to *parse*, but `value.some(x)` parses as a CallExpression and
`value.trim().length > 0` as a BinaryExpression; both then throw inside
`evaluateNode`, which `evaluateExpression` reports as `false` — pinning the
field permanently invalid and logging on every validation run. That is exactly
the failure the guard existed to prevent, and it hit both examples in the doc
comment above it. `RulesEngine.tryEvaluateExpression` now distinguishes "threw"
from "returned falsey"; `evaluateExpression` delegates to it, so its
false-on-error contract is unchanged for existing callers.
- `required: true` was bypassed by an explicit `minItems: 0`, which applied only
`.min(0)`. Anything below one is no effective constraint, so required applies
its own.
- checkbox, switch and radio rendered FormFieldError without marking the control
invalid, unlike every other control.
- The serializer dropped `valueType`, so a round-tripped custom field fell back
to `z.any()` and lost `required` silently.
BREAKING CHANGE: a required string field now rejects whitespace-only input.
Previously ' ' satisfied `required: true` on the resolver path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…vert leftovers Review follow-up (BenGSchulz + Copilot). StringListField hand-rolled its own label — a span plus RequiredIndicator plus InfoTooltip — which is precisely the duplication that moving the tooltip onto FormFieldLabel was meant to end, and it would have drifted from it. It now uses FormFieldLabel. The Add button moves below the rows as a left-aligned text variant, per design guidance. Leftovers from the controlled-host revert: isInitializedRef was written on every render and read nowhere since the watch moved to initializingRef; the zodSchema comment still mentioned hosts owning validation; the watch comment still described suppressing onValuesChange during a values sync-in rather than the plugin fan-out during initialization's reset; the comment above allCustomComponents was duplicated, with the first half still naming the removed components prop. Also drops an unused act import that the package tsconfig could not catch, since it excludes test files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The conflict in SelectTrigger's class string was resolved by merging both sides' tokens, which re-added future:text-muted-foreground — the class main deliberately removed in 'use foreground text for populated field triggers'. Dropping it again, so this branch's only change to the trigger is the aria-invalid styling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Two moderate dialog issues remain: impact ordering and duplicate list keys.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (2)
packages/apollo-react/src/canvas/components/Guardrails/guardrail-remove-dialog.tsx:96
- The impact list is rendered in tool-first order, but the corrected host contract calls for scopes before tools. With
affectedScopes={['Llm']}andaffectedToolNames={['Send email']}, this producesSend email, thenLLM callsinstead of the intended scope-first order; the story/test currently encode that regression as well. Render the scope entries before the tool entries and update the expected order.
packages/apollo-react/src/canvas/components/Guardrails/guardrail-remove-dialog.tsx:95 toolNamesandscopesshare the same<ul>, but their keys use the sameindex-valueformat. If a tool name equals a scope at the same index (for example, a tool namedAgentalongside theAgentscope), React emits duplicate-key warnings and can reconcile the wrong<li>when the impact changes. Prefix the key by list kind so the two namespaces cannot collide.
- Files reviewed: 102/103 changed files
- Comments generated: 0 new
- Review effort level: Lite
Running biome --write across the whole forms directory reformatted imports in six files this PR never touches semantically (data-fetcher and its test, form-examples, form-plugins, rules-engine.test, schema-viewer). Reverted to main, so the diff only carries files with real changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moves the guardrails family out of apollo-wind's custom/ prototype shelf into apollo-react per the #1107 review decision: canvas-adjacent (MUI-free, built on wind primitives and the forms/ MetadataForm engine), exported from the canvas components barrel plus a narrow ./canvas/guardrails subpath. Strings move to lingui: useSafeLingui labels-hooks with explicit guardrails.* ids replace the wind-local catalogs and loader; the 60 keys ship translated in the shared canvas catalog for 13 locales (ru falls back to English per key). Localized templates that cross into plain-string APIs are ICU messages formatted with sentinel values, preserving the {{token}} convention. The family's Tailwind classes ride the existing tailwind.canvas.css scan; adds class-variance-authority (dep) and jest-axe (dev, matcher registered in the shared test setup). Also silences Radix's aria-describedby warning on the description-less builder dialog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ugin seam Follow-up review (BenGSchulz). `GuardrailValidatorForm` keeps its controlled public contract — `parameters`/`onChange` plus host-owned `errors`/`onClearError` are unchanged for both consumers — but it no longer asks `MetadataForm` for controlled props, which that PR removes. The translation now lives in one named place, `useMetadataFormBridge`: a `FormPlugin` that declares the custom components (honoured from the first paint), reports user edits through `onValueChange`, pushes host values in per field via `context.form.setValue` (deep-equal guarded, and suppressed while syncing so a sync-in never echoes back as a user edit), and applies host errors as `type: 'external'`. Keeping it in one hook is the point: `context.form` is an unlabelled backdoor, and a reviewer who greps for a controlled prop would never find a scattered `setValue`. Also fixes a live bug Ben spotted: number parameters carried `min`/`max` only as DOM attributes, which browsers enforce on native form submission — something this form never does, since the host owns saving. An out-of-range guardrail was therefore saveable. The range is now declared in the field's `validation` (with `mode: 'onChange'`, or it would never be evaluated), and `getOutOfRangeParameterIds` gives hosts the same shape they already use for `getRequiredEmptyParameterIds` so Save can be gated on it. One test expectation moved: the enum-list combobox is now named by its visible field label rather than its placeholder, because the renderer associates them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Two unresolved moderate findings remain: add overlay non-dismissal coverage and reconcile the required destructive confirm variant.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (2)
packages/apollo-react/src/canvas/components/Guardrails/guardrail-remove-dialog.tsx:141
- The component contract documents that backdrop/outside clicks must not dismiss this destructive confirmation, but the dialog suite only covers Cancel and Escape. Please add an interaction test that clicks the overlay and asserts
onCancelis not called and the alert dialog remains open; otherwise a Radix/wind behavior change could silently reintroduce dismissal.
packages/apollo-react/src/canvas/components/Guardrails/guardrail-remove-dialog.tsx:191 - This implementation intentionally renders the remove action with the default accent variant, but the PR's stated “What ships” contract and QA-visible behavior require
Button variant="destructive". Please reconcile the shipped behavior with the PR/design decision before merge: either restore the destructive variant or update the description and acceptance criteria to the accent behavior.
- Files reviewed: 102/103 changed files
- Comments generated: 0 new
- Review effort level: Lite
…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>
`ImpactList` keys its items by index plus value: two tools can carry the same display name, and neither list is reordered or filtered, so the index is a stable part of the key rather than a warning waiting to happen. Wrote down two things a reviewer would otherwise ask about. The `container` union restates wind's `PortalContainerOverride`, which the wind barrel does not export, so the comment names the type the two have to stay in step with. `TEMPLATE_TOKENS` is one map for every label set, so the form and builder strings receive a `toolName` they never declare; the comment says it is inert.
…dialog [AL-577] Prefix the two `ImpactList` key sets by kind, and say in the comment what that is and is not for. The review asked for it as a collision fix; it is not one. The two maps are separate child arrays of the one `<ul>` and React scopes keys per array, so a tool named `Agent` at index 0 never collided with the `Agent` scope at index 0. Checked by spying on the duplicate-key warning: silent for that case, and it does fire for a real duplicate inside one array. The prefix stays because it makes the keys self-describing, not because it fixes anything. The i18n orphan scan also covers `ru`. Deliberately empty is not licence to keep a stale id; the coverage check still leaves `ru` out.
…og [AL-577] Both hosts reported the filled destructive Remove as a regression when they tested the shared dialog with the flag on: neither product colours a guardrail removal and both use the blue accent for it. The dialog carries the weight of the action through its title and impact lines instead. A test pins the variant, since this is the second decision on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
245e023 to
98a0f04
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
The changes span shared UI behavior, localization, accessibility, and host integration, warranting final human review.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
- Files reviewed: 97/98 changed files
- Comments generated: 0 new
- Review effort level: Lite
AL-577, phase 1 of the AL-526 guardrails convergence. Adds
GuardrailRemoveDialogto theapollo-react guardrails family: the confirmation step before a guardrail is removed, with the
impact of the removal spelled out.
Review only the last five commits (
8183f35cthe dialog,ea87e66athe test that coversthe localized interpolation path,
acde8bcethe review fixes,4449d929the second pass,98a0f049the accent confirm button). Thetwenty below them are
#1107 (
feat/apollo-wind-guardrail-validator-form), #1138(
feat/apollo-react-guardrails-family) and #1139 (feat/apollo-react-guardrail-definitions-layer),which this branches off. Nothing is taken from #1140 (list) or #1147 (palette); this stream is
a leaf off #1139 and can merge in any order relative to them.
Rebased onto #1139's current tip on 2026-09-11, conflict-free.
Jira AL-577's description is pre-revision (it still says an apollo-wind component under
components/custom/guardrails/with component-scoped locales). Built per the 2026-09-09revision instead: apollo-react
src/canvas/components/Guardrails/, strings as lingui ids inthe shared canvas catalog.
What ships
guardrail-remove-dialog.tsxGuardrailRemoveDialog,GuardrailRemoveDialogPropsi18n.ts(extended)GuardrailRemoveDialogLabels,GUARDRAIL_REMOVE_DIALOG_EN_LABELS,GUARDRAIL_REMOVE_DIALOG_EN_MESSAGES,resolveGuardrailRemoveDialogLabels,useGuardrailRemoveDialogLabelsPlus
guardrail-remove-dialog.stories.tsx(Components/UiPath/Guardrail Remove Dialog, 6stories),
guardrail-remove-dialog.test.tsx(16 tests), a newi18n.test.ts(9, catalog parity),a README section, and 7
guardrails.remove-dialog.*ids in all 13 catalogs.Built on wind's
AlertDialog. Props:open,guardrailName,toolName?,remainingToolNames?,remainingScopes?,affectedToolNames?,affectedScopes?,onConfirm,onCancel,formatScope?,container?,labels?,className?.The design decisions worth knowing:
both products already describe both:
affected*is what a full removal also takes theguardrail off,
remaining*is what survives a tool-scoped removal. Flow computes them fromone guardrail plus an
isToolOnAgentflag; that branch converges by filling one pair or theother. All four default to empty, so a plain confirmation needs neither.
the tool from
matchNames, then drops theToolscope, then deletes; Flow callsremoveToolFromGuardrail. The dialog only renders the description of the outcome.onConfirm/onCancelare intents and neither closes the dialog;openis controlled.onCancelcovers Cancel and Escape and fires exactly once per dismissal.Button, notAlertDialogAction. Radix's action is aclose button, so it drives
onOpenChange(false)on top of the click and the dialog reports acancel with every confirm. Flow's dialog does that today. A test pins it. If you would rather
keep the primitive, the composition that also works is
AlertDialogActionwithonClick={(event) => { event.preventDefault(); onConfirm(); }}: Radix'sClosecomposes theclick with
composeEventHandlers, which skips its ownonOpenChange(false)when the event isdefaultPrevented. The same test passes either way. Say which you prefer.destructive(245e0236). This shippeddestructive; both hosts reported the red as a regression when they tested the shared dialog
with the flag on, so it is now the same filled accent both products use for this action
today. A test pins the variant, since this is the second decision on it.
formatScope, matchingGuardrailList'sformatScopes. Scope vocabulary is product-owned and both products already hold the mapping.asChildonAlertDialogDescription, so an alert dialog announces what the removal costs rather thanonly its first sentence. It also fixes the
<p>-inside-<p>nesting Flow has today.container?picks the portal target: omit to inherit wind'sPortalContainerProvider(the wind
AlertDialogContenthonours both), pass an element, or'body'to forcedocument.bodyeven under a provider, which is what preserves Agents' deliberate escapefrom its shadow root. The union restates wind's own
PortalContainerOverride, which the windbarrel does not export; a comment names the type the two have to stay in step with, and
exporting it from wind is a separate tiny PR if you want one.
onTrack, despite Confluence §7.5: same call as feat(apollo-react): guardrail list section [AL-575] #1140. The two products' eventtaxonomies do not overlap and both adapters wrap the intents anyway.
Review fixes (
202d27e3)ImpactListkeys by index plus value.key={name}alone warns when two tools carry thesame display name. Neither list is reordered or filtered, so the index is a stable part of
the key.
containerunion andits wind counterpart (above), and
TEMPLATE_TOKENS, which is one map shared by every labelset, so the form and builder strings receive a
toolNameplaceholder they never declare. Itis inert; the comment says so rather than leaving it to be rediscovered.
No behaviour changed, so the test totals below are unchanged by this commit.
Copy: converged, with no English changes
All 7 strings exist in both products with identical English, so the convergence costs
neither product a wording change. Harvested 1:1 into the 12 translated locales at 100%
coverage;
ruleft empty per convention (both products ship it empty too). English parityagainst both products was asserted by the harvest script before writing, and
i18n.test.tsasserts catalog-to-source parity both ways plus orphan ids in every locale.
titleconfirm-promptalso-applicableremoved-for-toolstill-applicablecancelremoveTranslations also matched between the two products in 83 of 84 locale/id pairs. The one
disagreement is Romanian "Remove": Agents'
common.removeis "Înlăturare", Flow's is"Eliminare". Took Flow's, because it matches the verb the dialog's own Romanian title uses
("Eliminați mecanismul de control"). One loc-review item.
QA-visible changes per host, once each adapter flips its flag
Agents (
Guardrails/GuardrailRemoveDialog, MUI):MUI
variant="contained" color="primary"becomes wind's defaultButton, so the colour doesnot change. (It was destructive red until
245e0236; see the resolved question below.)the dialog. Radix's alert dialog deliberately refuses outside-click dismissal for a
destructive confirmation; Cancel and Escape still dismiss. Flow already behaves this way.
That is Radix's own default for an alert dialog, not something this code sets; a test pins
it so a primitive upgrade cannot move it quietly.
The dialog lists tools first, then scopes, and the tests pin that order.
GuardrailsListingSection.handleRemoveGuardrailswaps the two arrays today (affectedScopesreceives
matchNames,affectedToolNamesreceives the other scopes), so what Agents showsright now is scopes first, with
Llmrendered raw instead of "LLM calls". That is the bug,not the contract. The typed props make the adapter pass them the right way round;
flag-on fixes it (§8.1 asked whether to preserve or fix: fixing).
Flow (
GuardrailRemoveConfirmDialog):AlertDialogActionalready resolves tobuttonVariants(), which is what the shared dialog'sButtonrenders. (It was destructivered until
245e0236; see the resolved question below.)onConfirmand thenonOpenChange(false)bothfire today). Not user-visible, but it removes a spurious second state write and, once
telemetry moves onto the same handlers, a spurious event.
<p>/<ul>nesting insideAlertDialogDescriptionis gone.Adapter notes for both: pass
formatScope(the mapping each product already owns), and passonly the scopes worth listing, as both products already exclude the
Toolscope when theremaining tools are listed by name.
Second-pass review fixes (
91f1e718)ImpactListkey sets are prefixed by kind (tool-,scope-), which the reviewasked for as a collision fix. It is not one, and the comment now says so: the two maps are
separate child arrays of the one
<ul>and React scopes keys per array, so a tool namedAgentat index 0 never collided with theAgentscope at index 0. Checked by spying on theduplicate-key warning, which stays silent for that case and does fire for a real duplicate
inside one array. The prefix stays because it makes the keys self-describing.
ru. Deliberately empty is not licence to keep a stale id init; the coverage check still leaves
ruout.Copilot read the impact-list order as a contract violation across three passes. It is not:
tools-first is intended and pinned by tests, and the "scopes first" it saw is the description of
Agents' current bug above, which this PR's typed props fix. That paragraph is reworded so the
next reader does not invert it.
Open questions
Destructive (red) confirm button.Resolved: dropped,245e0236. It shippeddestructive and both hosts independently reported the red as a regression against the blue
accent they use for this action today, after testing the flag-on dialog (Agents#6238 and
flow-workbench). The confirm is now wind's default
Button, which matches MUIcontained/primaryin Agents andbuttonVariants()in Flow, so neither product's buttonchanges colour. A test pins the variant.
Does that leave the family inconsistent with feat(apollo-react): guardrail list section [AL-575] #1140's
text-destructiveRemove? No, andit removes the one inconsistency there was. The rule both products already follow is
destructive tint on a ghost trigger, never a filled destructive button, and Flow's own
DeleteButtonwrites it down: "Always ghost: a filleddestructivebutton is the loudestthing on the panel, which removing one list row is not." feat(apollo-react): guardrail list section [AL-575] #1140's row button is that ghost
trash with Flow's four-scope tint class verbatim, so it is on the right side of the rule; the
filled confirm was the only place the family went further than either product. feat(apollo-react): guardrail list section [AL-575] #1140 is
untouched here. The asymmetry that does remain is feat(apollo-react): guardrail list section [AL-575] #1140's alone and predates this: Agents'
kebab Remove item carries no tint at all today, so the red trash is QA-visible there.
Should
formatScopedefault to the family's own scope labels rather than the raw wirevalue?
guardrails.builder.scope-agent-label/-llm-label/-tool-labelalready existin the canvas catalog with exactly the wording both products use. Raw is what shipped, to
match
GuardrailList'sformatScopesand to keep scope vocabulary product-owned, but itis the one prop an adapter can forget and get a visible regression (
Llm).Agents' corner close (X) and backdrop dismissal are gone (above). Confirm that is
acceptable, or the dialog needs a
dismissibleescape hatch.mergeLabelsini18n.tsis introduced here byte-identically to the helper feat(apollo-react): guardrail list section [AL-575] #1140 andfeat(apollo-react): add-guardrail palette [AL-576] #1147 each added, so the three streams collapse onto one copy on merge rather than four
copies of the same loop. It is the only pre-existing code this commit touches, and it is a
pure refactor of
resolveGuardrailBuilderLabels/resolveGuardrailFormLabels(two newtests cover them). Say the word if you would rather see it land only once, in whichever
stream merges first.
i18n.test.tsis new on this stream and feat(apollo-react): guardrail list section [AL-575] #1140 / feat(apollo-react): add-guardrail palette [AL-576] #1147 each add their own copy of the file.Merging keeps all three suites; nothing else in it collides.
Verification
dialog including the focus trap, initial focus, Escape and the portal
container. One testrenders under
ApI18nProvider locale="ja"and asserts the guardrail name and tool name landinside the Japanese catalog strings, which is the only end-to-end cover for the two-stage
{{token}}sentinel convention this family uses for templates.tsc --noEmitclean, biome clean on every touched file,rslib buildclean, no zod in thefolder's
.d.ts, no icon churn, nolocales/*.tschurn (gitignored), nopnpm-lock.yamlchange (this adds no dependency).
noEmitandincludelimited to this folder reports no errors in this PR's files. Theten it does report are feat(apollo-react): guardrails component family under canvas #1138's own, present on its head:
guardrail-builder.stories.tsx:294,seven TS2532 in
guardrail-builder.test.tsx,guardrail-form-layout.test.tsx:37andguardrail-validator-form.test.tsx:1187.lingui compilereports nothing on the new ids. (It fails on three pre-existing Germanplurals in unrelated
canvas.json_value_panel.*/stage-node.*ids, unchanged here.)canvas/hooks/useStorageStateandcanvas/utils/Storage, both because Node 24'sexperimental
localStorageshadows happy-dom's. Not related.