Issue #3586689: Add an #allow_duplicates option to the ai_tools_library element

Description

Closes #3586689

Adds an #allow_duplicates option to the ai_tools_library form element, so a consumer can let the same tool be selected more than once and configure each occurrence separately. Defaults to FALSE, so nothing changes for existing consumers.

Why: the element treated its value as a set of plugin ids — a tool was either selected or it was not. [#3552038] in AI Agents needs the same tool several times, each occurrence with its own function name and settings, so one generic tool can be offered to the LLM more than once with different configuration. That MR is blocked on this: without the option, the feature only works for programmatically written config. The first iteration worked around it by unbinding this element's own JavaScript and patching its render array from an #after_build in the consuming module, which does not belong in contrib.

Where to look:

  • ToolsLibrary::processToolsLibrary() — with the option on, selected items are keyed by position rather than by tool id, so repeats each render as their own item instead of overwriting one another. #instance_index carries the position to the theme.
  • ToolsLibrary::valueCallback() — normalises to a list, and collapses repeats when the option is off.
  • ai_tools_library_form_element.js — the remove button uses data-instance-index (and data-ai-tools-library-allow-duplicates on the hidden field) to remove one occurrence rather than every copy of the tool.
  • Numbering is deliberately conditional: a tool that appears more than once renders as Calculator (1) / Calculator (2), but a tool selected only once keeps its plain name, so agents that never repeat a tool are not renamed.

Also fixes a latent bug on the default path. The modal appends its selection to the hidden field (setToolsFieldValue() concatenates) and currentSelection is cleared on dialog:afterclose, so the checkboxes start unchecked on the next visit. Re-selecting an already-selected tool therefore submitted its id twice. Nothing collapsed that — processToolsIds() does not deduplicate — while the rendered items were keyed by tool id and silently merged. The user saw one item; the stored value held two. Now collapsed in both valueCallback() and processToolsLibrary().

No breaking API changes. Additive: #allow_duplicates (bool, default FALSE), the instance_index theme variable (default NULL), and two data attributes. The one behaviour change on the default path is that a repeated id is collapsed instead of passed through — that is the element's documented contract, so a consumer relying on the repeat was relying on the bug.

Branch note: this targets 1.4.x because the blocked consumer requires drupal/ai ^1.4.0. All touched files are byte-identical on 1.5.x/1.x, so it merges forward without conflicts.

Testing instructions

Automated:

ddev exec 'cd web && ../vendor/bin/phpunit -c core/phpunit.xml.dist modules/contrib/ai/tests/src/Unit/Element/ToolsLibraryTest.php'
ddev exec 'cd /var/www/html && vendor/bin/phpunit -c phpunit.xml web/modules/contrib/ai/tests/src/FunctionalJavascript/AiToolsLibraryElementTest.php'

Manual, using the ai_test module's form element page:

  1. Enable the test module: ddev drush en ai_test -y
  2. Visit /admin/config/ai/test-form-elements?tools=ai:calculator,ai:calculator&allow_duplicates=1 → two separate items, labelled "Calculator (1)" and "Calculator (2)".
  3. Remove the first one → one item remains, the numbering is gone, and the hidden value holds a single ai:calculator. The remaining occurrence is still removable on its own.
  4. Visit /admin/config/ai/test-form-elements?tools=ai:calculator,ai:calculator (no allow_duplicates) → one item only, hidden value collapsed to a single id. This is the regression guard for existing consumers.
  5. Visit /admin/config/ai/test-form-elements?tools=ai:calculator,ai:weather&allow_duplicates=1 → neither is numbered, because neither repeats.
  6. With allow_duplicates=1, click "Select tools", tick a tool that is already selected, confirm → a further occurrence is added. Repeat without allow_duplicates → still one item.
  7. Submit the form in each case and check the printed value matches what is displayed.

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 #3586689

Merge request reports

Loading