Issue #3586655: "Add interactive refinement capability to Field Widget Actions 2.x"

Summary

Ports the interactive refinement capability for Field Widget Actions Automation from 1.x (issue #3586468 (closed), MR !1629 (merged)) to 2.x.

As noted in the issue, this was not a straight cherry-pick: in 2.x an ai_automator config entity holds an automator_types plugin collection (each entry its own configured plugin instance) instead of 1.x's flat rule + plugin_config. The port re-implements the same feature against that architecture.

Changes

  • New Traits/AutomatorFieldWidgetActionTrait.php , extracted from AutomatorBaseAction so both it and the new AutomatorRefinableBaseAction share the automator pipeline (service injection, config form, field-input helpers) without duplicating 2.x's own additions (resetOptionsWidgetCache(), the automator_types-aware getAutomatorsOptions()).
  • New AutomatorRefinableBaseAction.php , base class for field widget actions with interactive refinement, extending FieldWidgetRefinableFormActionBase (from drupal/field_widget_actions). hasUsableSource() / refineContent() read automator config through AiAutomatorInterface::getAutomatorTypes() (2.x's single-automator-type convention) instead of 1.x's flat plugin_config.
  • Email, ImageAltText, ImageFilename, Text, SummaryTextareaWithSummary, ListString now extend AutomatorRefinableBaseAction, gaining the modal refinement UI.
  • HTML entity handling: AiPromptHelper::renderPrompt() wraps prompts in {% autoescape false %}; RuleBase::decodeLabel() / decodeValueArray() decode and flatten model output before matching it against field options (Lists, Options, Taxonomy).
  • composer.json: bumped drupal/field_widget_actions from ^1.0.x-dev to ^1.4, matching what 1.x already requires, the refinement API (FieldWidgetRefinableFormActionBase) isn't in the older constraint.
  • New/updated tests: AutomatorRefinableBaseActionTest, ListStringTest, LabelEntityDecodeTest, AiPromptHelperTest, plus additions to FieldWidgetActionFormInputTest and RuleBaseDecodeValueArrayTest.

Bugs found and fixed during manual testing (2.x-specific, not present in the 1.x port)

Static analysis and the initial automated tests passed, but manual browser testing surfaced three real defects, each with its own regression test (verified to fail without the fix):

  1. refineContent()/hasUsableSource() read the wrong config shape. RuleBase::getConfiguration() in 2.x returns the full plugin-collection entry (uuid, id, weight, settings, label), not the flat settings 1.x had — so ai_provider/mode/token must be read from getConfiguration()['settings']. Before the fix, refinement instructions silently had no effect (empty ai_provider hit an early return every time).
  2. The refinement modal's "Generate" button also ran the automator a second time via a stray #submit handler. AutomatorFieldWidgetActionTrait::actionButton() unconditionally attached runAutomatorSubmit regardless of enable_refinement. Since Drupal runs all #submit handlers before the #ajax callback, the automator ran once synchronously (writing a real value into the field) before the modal's own generateContent() ever got a chance to run — making the empty-source hint unreachable and doubling AI provider calls on every click. Fixed by only attaching that handler when refinement is disabled.
  3. Label/key mapping in ListString broke inside the modal. The modal reconstructs the plugin fresh from stored config on every request and never calls setFieldDefinition() on it, so getFieldDefinition() is NULL there — keyToLabel()/labelToKey() need a fallback. Fixed by reading $context_data['target_element_field_settings']['allowed_values'], which field_widget_actions captures at the moment the modal opens.
  4. refineContent() couldn't resolve the provider/model for automators saved through the current admin UI. The AI Automator edit form now saves provider/model under a standardized provider_config.provider / provider_config.model structure; the flat ai_provider/ai_model keys are kept by RuleBase::getProvider()/getModel() only as a backwards-compatible fallback for older automators. refineContent() read only the legacy flat keys, so refinement silently no-op'd for every automator created through the real UI. Same fix pass also corrected the field-requirements prompt lookup (token is present-but-empty for base-mode automators, so a ?? chain never fell through to prompt).

Out of scope

Two pre-existing issues surfaced during manual testing, neither touched by this MR:

  • AiAutomatorEntityModifier intermittently duplicates a list_string automator's output into two identical field items. Reproduces via a plain $node->save() with zero field_widget_actions involvement — pre-existing ai_automators core behavior.
  • The AI Automator edit form doesn't clean up model-incompatible provider settings on model change. ai_provider_openai::getModelSettings() renames max_tokens → max_completion_tokens and drops reasoning_effort for reasoning-model families (gpt-5/o1/o3/o4), but this only shapes the form schema — a value already saved under the old model (or added by the form regardless of the selected model) stays in provider_config.config and gets sent to the API unconditionally, causing "Unsupported parameter" errors. Lives entirely in ai_provider_openai / the automator edit form, unrelated to this port.

Testing instructions

  1. Create an AI Automator Setup for content_type → entity → field_content with rule llm_text_long , point the base field to the title, and add a prompt something like "You must write a short blog paragraph about: {{ context }}. Return ONLY the paragraph text, nothing else."
  2. Enable the Field Widget Action with refinement Go to /admin/structure/types/manage/blog/form-display Click the gear on field_content, under Field Widget Actions enable "Automator Text Suggestion" Pick the automator, check "Enable Refinement", save
  3. Edit an entity Click the AI button next to the content field With refinement ON → refinable modal should appear With refinement OFF → content fills directly into field

Checklist

  • I have linked the related issue in the MR title or description
  • I have performed a self-review of my own code
  • I have added or updated tests, or explained in the description why this change is not covered by tests
  • I have updated documentation for any new or changed functionality
  • I have written testing instructions and verified them locally
  • I have noted any required post-merge steps (config imports, cache rebuilds, manual changes)
  • This MR contains no breaking API or hook changes, or they are explicitly documented in the description

AI Compliance

Note

Check the one that best describes your usage, or leave all unchecked if AI was not significantly used.

  • AI Assisted Code
    Mainly written by a human; AI used for autocomplete or partial generation under full human supervision.
  • AI Generated Code
    Mainly generated by AI, reviewed and approved by a human before this MR was created.
  • Vibe Coded
    Generated by AI and only functionally reviewed before this MR was created.

Closes #3586655 (closed)

Edited by Juan Correa

Merge request reports

Loading