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 fromAutomatorBaseActionso both it and the newAutomatorRefinableBaseActionshare the automator pipeline (service injection, config form, field-input helpers) without duplicating 2.x's own additions (resetOptionsWidgetCache(), theautomator_types-awaregetAutomatorsOptions()). - New
AutomatorRefinableBaseAction.php, base class for field widget actions with interactive refinement, extendingFieldWidgetRefinableFormActionBase(fromdrupal/field_widget_actions).hasUsableSource()/refineContent()read automator config throughAiAutomatorInterface::getAutomatorTypes()(2.x's single-automator-type convention) instead of 1.x's flatplugin_config. Email,ImageAltText,ImageFilename,Text,SummaryTextareaWithSummary,ListStringnow extendAutomatorRefinableBaseAction, 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: bumpeddrupal/field_widget_actionsfrom^1.0.x-devto^1.4, matching what1.xalready requires, the refinement API (FieldWidgetRefinableFormActionBase) isn't in the older constraint.- New/updated tests:
AutomatorRefinableBaseActionTest,ListStringTest,LabelEntityDecodeTest,AiPromptHelperTest, plus additions toFieldWidgetActionFormInputTestandRuleBaseDecodeValueArrayTest.
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):
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 — soai_provider/mode/tokenmust be read fromgetConfiguration()['settings']. Before the fix, refinement instructions silently had no effect (emptyai_providerhit an early return every time).- The refinement modal's "Generate" button also ran the automator a second time via a stray
#submithandler.AutomatorFieldWidgetActionTrait::actionButton()unconditionally attachedrunAutomatorSubmitregardless ofenable_refinement. Since Drupal runs all#submithandlers before the#ajaxcallback, the automator ran once synchronously (writing a real value into the field) before the modal's owngenerateContent()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. - Label/key mapping in
ListStringbroke inside the modal. The modal reconstructs the plugin fresh from stored config on every request and never callssetFieldDefinition()on it, sogetFieldDefinition()isNULLthere —keyToLabel()/labelToKey()need a fallback. Fixed by reading$context_data['target_element_field_settings']['allowed_values'], whichfield_widget_actionscaptures at the moment the modal opens. 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 standardizedprovider_config.provider/provider_config.modelstructure; the flatai_provider/ai_modelkeys are kept byRuleBase::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 (tokenis present-but-empty for base-mode automators, so a??chain never fell through toprompt).
Out of scope
Two pre-existing issues surfaced during manual testing, neither touched by this MR:
AiAutomatorEntityModifierintermittently duplicates alist_stringautomator's output into two identical field items. Reproduces via a plain$node->save()with zerofield_widget_actionsinvolvement — pre-existingai_automatorscore behavior.- The AI Automator edit form doesn't clean up model-incompatible provider settings on model change.
ai_provider_openai::getModelSettings()renamesmax_tokens→max_completion_tokensand dropsreasoning_effortfor 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 inprovider_config.configand gets sent to the API unconditionally, causing "Unsupported parameter" errors. Lives entirely inai_provider_openai/ the automator edit form, unrelated to this port.
Testing instructions
- 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."
- 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
- 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)