Canvas AI: Add the edit_components AI tool (+ kernel test)
## Summary Add a new AI tool `canvas_ai:edit_components` (class `EditComponents`) to `canvas_ai`, ported from the POC (MR !1214). It updates the props of components already on the page, keyed by UUID, reusing the same validator as `place_components`. It lands **inert** — no agent lists it yet. Add a kernel test. ## Why (this sprint) `edit_components` is the second modification tool of the hop loop: after a component is placed, the agent tweaks its props (copy, labels, values) by UUID without re-placing it. Like `place_components` it is deterministic PHP, fully testable on its own, so it can land now in parallel. It is additive and inert on landing, so it does not touch the live editor. - Roadmap: https://git.drupalcode.org/project/canvas/-/work_items/3591777 - Reference implementation (POC, no test): https://git.drupalcode.org/project/canvas/-/merge_requests/1214 ## What to build Port `EditComponents` from the POC to `canvas_ai` under the final ID `canvas_ai:edit_components`. Port it largely as-is (apart from any small optimizations). - **Argument.** A single YAML string. Name it **`prop_changes_yaml_keyed_by_component_uuid`** (the POC calls it `component_updates`; the descriptive name states the shape and matches the `_yaml` convention from `place_components`). Format: a top-level `component_edits` map keyed by component UUID, each value a set of `prop_name: value` pairs. The `component_edits` root keeps the model from spreading UUIDs as tool-argument names; unwrap it, falling back to the parsed value if the root is omitted. - **Interfaces.** `final class EditComponents extends FunctionCallBase implements ExecutableFunctionCallInterface, AiAgentContextInterface, BuilderResponseFunctionCallInterface`. - **Output.** `getStructuredOutput()` returns `['component_updates' => <uuid-keyed prop map>]` — no nodePaths and no `PlacementDto` (edits are in place, unlike `place_components`). The frontend applies the prop updates by UUID. - **Validation it adds over `place_components`.** A UUID-existence check, then it merges each update over the component's existing resolved props and runs the same `AiResponseValidator::validateComponentStructure()`. Because updates merge over existing props, invalid prop names / value shapes are caught while required-prop errors do not spuriously fire. ### Methods (class::method → logic) - `EditComponents::execute()` — permission check (`CanvasAiPermissions::USE_CANVAS_AI`) → parse the YAML → unwrap `component_edits` (fallback to the parsed root) → `validateComponentUpdates()` → `setStructuredOutput(['component_updates' => …])` and a success message. On exception, set a `Failed to edit components: …` output (self-correctable, not fatal). - `EditComponents::validateComponentUpdates(array $component_updates)` — **new** — read the running layout from `CanvasAiTempStore::CURRENT_LAYOUT_KEY`, resolve it with `getComponentsByUuid()`; for each edited UUID, error `"Component <uuid> was not found on the page."` if absent, else merge `prop_updates + existing props` into a `[component_id => ['props' => merged]]` group; throw on any not-found error, otherwise hand the groups to `AiResponseValidator::validateComponentStructure()`. - `CanvasAiPageBuilderHelper::getComponentsByUuid(array $current_layout)` — **new, additive** (+ its private recursive `collectComponentsByUuid()`) — walk the layout's regions and slots and return a `uuid → { component_id, props }` map. Not present in live; port it alongside. ### Services All services this tool uses already exist in live `canvas_ai` (`canvas_ai.page_builder_helper`, `canvas_ai.tempstore`, `canvas_ai.response_validator`, plus `logger.factory` / `current_user`). No new service class — only the additive `getComponentsByUuid()` / `collectComponentsByUuid()` methods on the existing helper. ### Inert on landing Do **not** add `edit_components` to any agent's tool list. It ships as an available plugin only; wiring it to the dev agent is a later issue. ## Tests New kernel test `modules/canvas_ai/tests/src/Kernel/Plugin/AiFunctionCall/EditComponentsTest.php` extending `CanvasKernelTestBase` (reuse the `SetAIGeneratedComponentStructureTest` fixtures / `getCurrentLayout()` / `normalizeErrorString()`). Cover: - **Happy path.** Valid edits against a seeded layout → assert `getStructuredOutput()` equals `['component_updates' => …]` and the success output. - **Root unwrap.** Works both with the `component_edits` root and with the bare UUID map (fallback). - **Permission.** Without the permission, running the tool throws. - **UUID not on page.** → `"Component <uuid> was not found on the page."` - **Prop validation on the merged result.** Invalid prop name / invalid value shape → the `validateComponentStructure()` error (assert exact strings, as the structure test does). ## Explicitly NOT doing - Not wiring `edit_components` to any agent. - Not building `set_page_value` / `get_component_details` — separate issues sharing the same validator/helper. --- _Issue generated with AI assistance._
issue