Canvas AI: Add the get_component_details AI tool + component-context helper (+ tests)
## Summary
Add a new **information tool** `canvas_ai:get_component_details` (class `GetComponentDetails`) and a backing service `CanvasAiComponentContextHelper` (`canvas_ai.component_context_helper`) to `canvas_ai`, ported from the POC (MR !1214). Given a list of component ids, the tool returns the description, props, and slots of exactly those components, so an agent can fetch full metadata only for the candidates it shortlisted. It lands **inert** — no agent lists it yet. Ships with a kernel test.
## Why (this sprint)
An agent first scans a lean catalog (id/name/description) and then fetches full metadata only for the components it intends to use — spending tokens in proportion to what it uses. `get_component_details` is the on-demand "give me props and slots for these ids" half of that split. It is deterministic PHP, additive, and inert on landing, so it can land now, tested, in parallel.
- Roadmap: https://git.drupalcode.org/project/canvas/-/work_items/3591777
- Reference implementation (POC): https://git.drupalcode.org/project/canvas/-/merge_requests/1214
## What the POC provides (reference to port)
- `modules/canvas_ai/src/CanvasAiComponentContextHelper.php` — the service (`getComponentCatalog()`, `getComponentDetails()`).
- `modules/canvas_ai/src/Plugin/AiFunctionCall/GetComponentDetails.php` — the tool. (Its docblocks mention `get_metadata_of_components`, a stale name — the real `function_name` is `get_component_details`.)
## What to build
### 1. New service `CanvasAiComponentContextHelper` (`canvas_ai.component_context_helper`)
Register it in `canvas_ai.services.yml` with `@canvas_ai.page_builder_helper` as its constructor argument. Three methods:
- **`getComponentContextArray(): array`** (private) — decodes `CanvasAiPageBuilderHelper::getComponentContextForAi()` (a YAML string) into the component-context array. The two public methods below use it. This is the one line that changes at graduation, when that source starts returning an array directly.
- **`getComponentDetails(array $component_ids): string`** — returns the `description`, `props`, and `slots` of the given components as YAML, keyed by id. If any id is not in the context, returns only the error `Component with id "@id" does not exist.` and no component data.
- **`getComponentCatalog(): string`** — returns the lean catalog (each component's `name` and `description` only) as YAML, keyed by id. Ships now; not consumed until Sprint 2.
Do not change `CanvasAiPageBuilderHelper::getComponentContextForAi()` — its `string` return type and the live `get_component_context` tool that consumes it stay untouched.
### 2. New tool `GetComponentDetails` (`canvas_ai:get_component_details`)
Port from the POC. In the `#[FunctionCall]` attribute: `group: 'information_tools'`, `module_dependencies: ['canvas_ai']`; mark the class `@internal`.
- **Class:** `final class GetComponentDetails extends FunctionCallBase implements ExecutableFunctionCallInterface, AiAgentContextInterface`.
- **Accepts:** one argument `component_ids_list` — a `string` with `multiple: TRUE` — the component ids to fetch (e.g. `sdc.canvas_test_sdc.my-hero`).
- **Calls:** `CanvasAiComponentContextHelper::getComponentDetails()` with the `component_ids_list` values.
- **Output:** `execute()` checks the `CanvasAiPermissions::USE_CANVAS_AI` permission (throws if the user lacks it), then passes the helper result to `setOutput()`. It sets **no structured output** — it does not implement `BuilderResponseFunctionCallInterface`, so its result is plain readable output and nothing is merged into a hop response.
### 3. Inert on landing
Do **not** add `get_component_details` to any agent's tool list. It ships as an available plugin only; wiring it to an agent is a later issue.
## Tests
One **kernel test** — `GetComponentDetailsTest` (`CanvasKernelTestBase`), modeled on `modules/canvas_ai/tests/src/Kernel/Plugin/AiFunctionCall/SetAIGeneratedComponentStructureTest.php`.
- **Setup:** the same module list, plus `ComponentSourceManager::generateComponents()` so real SDC and block components exist, and a JS component created via `CreateTestJsComponentTrait`. Create a privileged user (`CanvasAiPermissions::USE_CANVAS_AI`) and an unprivileged user.
- **Valid ids** → invoke the tool with the ids of the SDC, the block, and the JS component; assert `getOutput()` contains each one's `description`, `props`, and `slots`.
- **Invalid id** → invoke with a made-up component id; assert `getOutput()` is `Component with id "@id" does not exist.` and contains none of the valid components' data.
- **No permission** → the unprivileged user triggers the "does not have the right permissions to run this tool." exception.
Assert `getOutput()`, not `getStructuredOutput()` — this is an information tool.
## Manual verification (UI)
1. Install the `ai_api_explorer` module (`web/modules/contrib/ai/modules/ai_api_explorer`).
2. Go to `/admin/config/ai/explorers/tools_explorer`. Confirm both `get_component_context` and the new `get_component_details` are listed.
3. Open the page in two tabs.
4. **Tab 1 — `get_component_context`:** select it, click **Run function**. The output lists every component with its id, props, and slots. Copy 2–3 component ids.
5. **Tab 2 — `get_component_details`:** select it — a `component_ids_list` field appears. Enter the 2–3 ids (one per line), click **Run function**. The output shows the props and slots of exactly those components.
6. **Invalid id:** enter one made-up id. The output states that id does not exist (not empty/null).
7. **One valid + one invalid:** the output names only the invalid id and returns no data for the valid one.
## Explicitly NOT doing (Sprint 2)
- **Narrowing `get_component_context`** to id/name/description and re-wiring it as the lean default information tool. It stays as-is. This issue does not touch `get_component_context`, the `getComponentContextForAi()` `string` signature, or any agent config.
- Wiring `get_component_details` to any agent.
---
_Issue generated with AI assistance._
issue