Issue #3586361: Decouple context item form descriptions from scope plugin descriptions.
Description
Adds getItemFormDescription() to AiContextScopeInterface so a scope plugin can
show different guidance on a context item form than on the scope overview and
settings pages. It defaults to getDescription(), so every existing plugin keeps
its current behaviour without an override.
AiContextScopeGlobal already needed this and worked around it privately: it
carried a protected getFormDescription() whose docblock read "Uses a line break
that is not safe for settings pages escaped with Html::escape(), so keep plain
text in getDescription() instead." That was invisible to the API, so no other
scope could do the same thing. This promotes it to a documented interface method.
Interface (src/Plugin/AiContextScope/AiContextScopeInterface.php)
- Added
getItemFormDescription(). - Rewrote the
getDescription()docblock. It previously said "Gets the scope description for context items" - the source of the confusion, since the overview and settings pages use it too. It now describes itself as the general description and records that both those pages escape it withHtml::escape(), so it must be plain text. - Updated the
$for_subscriptionparameter docs onbuildValueForm()and added@seecross-references between the three description methods.
Base class - getItemFormDescription() returns getDescription();
buildValueForm() selects it for the item form.
AiContextScopeSiteSection, AiContextScopeTag - same one-line change; both
override buildValueForm() and duplicated the ternary.
AiContextScopeTargetEntity - its #description sits in the branch reached
only for item forms, so it now uses the new method.
AiContextScopeGlobal - protected getFormDescription() becomes
public getItemFormDescription(), warning and <br> unchanged. Removing "Saving
the form with this enabled will clear previous scope settings." from the plugin
attribute description is no longer part of this MR: #3586359 (closed) / !234 (merged) landed
that same change on 1.0.x, so this branch inherits it after the rebase. The
method docblock keeps !234 (merged)'s wording and adds why the line break matters.
AiContextScopeOverviewForm - the overview page read
getPluginDefinition()['description'] ?? '' directly, bypassing
getDescription(). That left the new extension point inoperative on that page: an
override was ignored, and a plugin declaring no description got an empty cell
rather than the label-derived fallback. It now calls $plugin->getDescription(),
which is what scope_api.md and the interface docblock already described.
Rendered text is unchanged for every in-tree plugin, confirmed in a browser.
AiContextScopeLanguage needed no change - its single-language branch renders
an info item with no #description, and its multi-language branch delegates to
parent::buildValueForm().
What the two descriptions look like
The split this MR formalises, on a real site. Both views already render this way on
1.0.x now that !234 (merged) has landed, so these illustrate the mechanism
getItemFormDescription() makes available to any scope plugin, rather than a
change introduced here.
Scope overview page - the general description, no save/clear warning
Context item form - the item form description, warning kept on its own line
Documentation - docs/developers/scope_api.md lists all three description
methods and gains a Choosing a description method section with a
where-it-appears / which-method table, plus a note that getDescription() must be
plain text while getItemFormDescription() may contain simple markup.
docs/developers/custom_scopes.md no longer describes the attribute
description as context-item-specific and links to that section.
Tests
tests/src/Unit/AiContextScopeItemFormDescriptionTest.php (new, 4 tests) covers
the default fallback using AiContextScopeUseCase, following the
direct-instantiation pattern already used by AiContextScopeAlteredValuesTest:
the method equals getDescription(); the fallback also holds for the
label-derived description when the plugin definition has no description key;
buildValueForm(..., FALSE) prints it; and buildValueForm(..., TRUE) still
prints getSubscriptionDescription(). The buildValueForm() test uses a fixture
whose general and item form descriptions differ, so it can tell which method
the form read - reverting AiContextScopeBase.php:194 fails it.
tests/src/Unit/AiContextScopeGlobalTest.php gains tests for the override. The
general-description test builds the plugin from the real #[AiContextScope]
attribute by reflection rather than from a fixture, so restoring the warning
sentence to the attribute fails it. testBuildValueFormUsesItemFormDescription()
was removed as redundant with !234 (merged)'s testBuildValueFormDescription(), which
asserts the same #description against the literal expected string and so also
catches a wording change.
tests/src/Kernel/AiContextScopePluginTest.php gains one test each for
AiContextScopeSiteSection, AiContextScopeTag and AiContextScopeTargetEntity.
All three override buildValueForm() and duplicate the description choice, so the
base-class test cannot reach them. Each uses a small subclass stub
(tests/src/Kernel/*ItemFormDescriptionStub.php) overriding only
getItemFormDescription(), leaving the real buildValueForm() to run. Reverting
SiteSection:123, Tag:116 or TargetEntity:237 fails the matching test.
Every test above was verified by breaking the production line it guards and confirming the test fails, then restoring it.
Backwards compatibility
Adding a method to AiContextScopeInterface is technically a BC break for a
third-party plugin implementing the interface directly instead of extending
AiContextScopeBase. Nothing in the module does that - the abstract base is the
only direct implementer and supplies a default - so no in-tree plugin needs an
override. Noting it because the interface is public API.
Overlap with #3586359 (closed) / !234 (merged)
!234 (merged) has landed on 1.0.x and this branch is rebased on top of it. Two
consequences for this diff:
- Removing the save-warning sentence from the Global plugin attribute is now upstream's change, so that hunk is gone from here. The acceptance criterion is still met, just not by this MR.
AiContextScopeGlobalTest::testBuildValueFormUsesItemFormDescription()has been removed. !234 (merged)'stestBuildValueFormDescription()asserts the same#descriptionagainst the literal expected string, so it catches strictly more than mine did, including a wording change. Keeping both was duplication.
The @defgroup ai_context_scope_api block !234 (merged) added to ai_context.api.php is a
signpost to the interface rather than a method reference - it mentions no
description method - so the where-it-appears / which-method table stays in
docs/developers/scope_api.md alongside the other method tables. Happy to move it
if you would rather that guidance lived in api.php.
Testing instructions
Set up per the template, then check out this MR branch and ddev drush cr.
Since !234 (merged) landed, steps 1-3 are regression checks rather than demonstrations.
1.0.x already renders this text the same way: !234 (merged) removed the sentence from the
Global attribute and kept the two-line item form description. What this MR adds is
the interface method, and the overview page resolving its text through
getDescription() - neither is visible without a third-party scope plugin. The
cache rebuild is no longer strictly required for plugin definitions, since this MR
changes no attribute; run drush cr anyway to clear render caches.
- Go to
/admin/config/ai/context/settings/scope(Settings > Scope). Every row has a description, and the Global row reads "Always include this context for every agent." without mentioning saving or clearing. Identical to1.0.x. What changed is that the page now resolves that text throughgetDescription(), so a plugin overriding the method is honoured and a plugin declaring nodescriptiongets the label-derived fallback instead of an empty cell. - Go to
/admin/config/ai/context/settings/scope/global. Same text, same absence of the warning. - Go to
/admin/config/ai/context/items/add. Expand Context Scope. The Global context checkbox description shows both lines: "Always include this context for every agent." then, after a line break, "Saving the form with this enabled will clear previous scope settings." - On that same form, expand Use Cases, Tags and Site Sections. They
read "The use cases / tags / site sections relevant for this context." -
unchanged from before this MR. This is the regression check: those plugins do
not override the new method, so they fall through to
getDescription(). - Go to
/admin/config/ai/context/settings/agents, edit an agent, and expand Scope Subscriptions. Descriptions still use the subscription wording ("Prioritize context items for these use cases."), and Global is absent - the form's own note confirms "These Scope Plugins Do Not Allow Subscriptions: Global, Target Entities".
Verified locally on Drupal 11.4.4 (standard profile) - all five steps performed in a browser.
Automated verification, all exit 0:
Measured after rebasing onto 1.0.x at 77c4a63. The earlier figures in this
description (163 / 519 / 156) predated that rebase, which brought in three
upstream commits adding tests of their own.
- Unit 167 tests
- Kernel 540 tests
- Functional 160 tests
- phpcs clean; PHPStan clean against the module's
phpstan.neon.dist - cspell reports no new words
Two notes for anyone reproducing this locally:
FunctionalJavascriptwas not run - it needs a Selenium/chromedriver service DDEV does not provide andMINK_DRIVER_ARGS_WEBDRIVERis empty inweb/core/phpunit.xml. This change touches no JavaScript.- The suite needs the module's whole
require-devinstalled. Withoutdrupal/diffanddrupal/ai_agents_debugger, 24 tests fail acrossAiContextScopeMapFieldDiffBuilderTest,AiContextItemScopeDiffTest,AiContextItemRevisionUiWithDiffTest,AiContextSchedulerIntegrationTestandAiContextAgentSettingsTest- none of them related to this change.
Checklist
- I have updated the MR title to use format:
Issue #1234: My issue title. - I have updated the MR description to include:
Closes #1234 - 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
Post-merge step: a cache rebuild is needed, because the #[AiContextScope]
attribute's description for the Global scope changed and plugin definitions are
cached. No config import or manual change is required.
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.
Claude Code was used to generate the code changes, both test classes, the documentation edits and this description. Every change was reviewed and applied by hand, the full test suite and static analysis were run locally, and all five testing steps above were performed in a browser before this MR was created.
Closes #3586361 (closed)

