Repository navigation
fix(workflow-form): let ui-udf-parameters show the form's renames and hides in its columns, and add a parameter to the card's operator - #8846
Conversation
Automated Reviewer SuggestionsBased on the
|
Backport auto-label reportThis
|
4d11c93 to
49e5528
Compare
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Filtered column headers mislabel the fixed Add parameter draft controls when Name or Value is hidden.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Updates Form View’s UDF parameter tables to respect column overrides and add parameters to the card’s operator, while preserving property-panel behavior.
Changes:
- Applies column renames, visibility overrides, and read-only locks.
- Routes parameter additions to the bound operator and refreshes affected cards.
- Adds unit and real-Formly regression coverage.
| File | Description |
|---|---|
| frontend/src/app/workspace/util/custom-formly-type.ts | Adds operator and read-only field helpers. |
| frontend/src/app/workspace/util/custom-formly-type.spec.ts | Tests field helpers. |
| frontend/src/app/workspace/component/workflow-form/workflow-form.spec-harness.ts | Adds parameter-sync mocks. |
| frontend/src/app/workspace/component/workflow-form/workflow-form.rendered.spec.ts | Provides the parameter-sync dependency. |
| frontend/src/app/workspace/component/workflow-form/workflow-form.component.ts | Marks fields and synchronizes declared parameters. |
| frontend/src/app/workspace/component/workflow-form/workflow-form.component.spec.ts | Tests synchronization, rebuilds, and field metadata. |
| frontend/src/app/workspace/component/ui-udf-parameters/ui-udf-parameters.component.ts | Handles overrides, operator targeting, and edit permissions. |
| frontend/src/app/workspace/component/ui-udf-parameters/ui-udf-parameters.component.spec.ts | Tests Form View and panel behavior. |
| frontend/src/app/workspace/component/ui-udf-parameters/ui-udf-parameters.component.html | Renders filtered columns and updated headers. |
| frontend/src/app/workspace/component/ui-udf-parameters/ui-udf-parameters.component.formly.spec.ts | Tests permissions and controls under real Formly. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
49e5528 to
94aff48
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #8846 +/- ##
============================================
- Coverage 91.81% 91.78% -0.03%
Complexity 5019 5019
============================================
Files 1277 1281 +4
Lines 56369 56787 +418
Branches 7364 7476 +112
============================================
+ Hits 51755 52123 +368
- Misses 2759 2793 +34
- Partials 1855 1871 +16
*This pull request uses carry forward flags. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
94aff48 to
8f86ca9
Compare
8f86ca9 to
01ace8f
Compare
01ace8f to
11d0214
Compare
…lls locked when the panel enables its form group (apache#8848) ### What changes were proposed in this PR? Closes apache#8847. The Python UDF operator's "UI parameters" table (apache#5043) infers each parameter's name and type from the `self.UiParameter(...)` declarations in the operator's code, so the widget disables those two columns' controls as it builds each row: a name typed into the table does not reach the code, and the parameter's value then never reaches it either (checked on main: the table's name changes, the code's does not). The lock does not survive on the operator property panel. `setInteractivity(true)` enables the whole form group -- one task after every render, and again whenever the workflow becomes modifiable, for instance when a run ends -- and formly mirrors a control's state back into `props.disabled`, so the one-time disable is simply undone. Which state a given panel opening shows depends on which of the two ran last: measured on main, the cells were editable on 4 of 5 openings, and always after a run. The Form View, which never enables a group wholesale, has always shown them locked, so the two views disagreed as well. The widget now holds its own lock. On each cell it locks it registers Angular's disabled-change hook of the control (`registerOnDisabledChange`, the same hook formly's mirror and the input's value accessor use): when a host enables the control, the hook disables it again in the microtask after that enable, and announces the status change as the enable did. Both details matter and were found on a running instance: - deferred to the microtask, because the input's value accessor is one more hook on the same control, registered after this one when the cell renders; disabling from inside the enable's own round of hooks left the accessor having been told "enabled" last, so the box was editable over a locked control; - announced rather than silent (`disable()`, not `disable({emitEvent: false})`), because ng-zorro's `nz-input` paints its `disabled` attribute from the control's status changes; a silent disable left it painting the box editable again at the next change detection. A cell the widget configures editable -- the Value column, or any cell once reconfigured -- is released from the hold, so whether it is editable stays the host's call: a read-only panel still locks everything, and a host that disables the whole field still reaches the Value cell. The frame is untouched: two other approaches were tried first and dropped, re-disabling from the frame after `enable()` (formly's mirror has already set `props.disabled` to false by then) and a formly expression on `props.disabled` (formly re-evaluates expressions on model changes only, and registers them at populate, so `enable()` never triggers one). ### Any related issues, documentation, discussions? Closes apache#8847. Split out of apache#8846 (the same table under the Form View's renames and hides), which is based on main and does not touch this; the two are independent and can land in either order. The table's fixed 250px columns clip Name and Type at the panel's default 260px width, which is why the cells have to be brought into view to see any of this; that is a separate layout bug, filed separately. ### How was this PR tested? Unit tests (vitest). The widget's spec: a locked cell's control is held locked when a host enables it, in the microtask after the enable and announcing the status change, while the Value cell is left to the host; a cell configured editable again is released from the hold; a cell the host disables again before the microtask is left alone; a control that is not a `FormControl` gets no hook, and a control is hooked once however often it is configured. A second spec stands the table up under real formly (`FormlyModule.forRoot`, a form, the builder row template formly-jsonschema produces, `nz-input` cells as in the app): the rows build with Name and Type locked and Value editable; a wholesale `form.enable()`, as the panel does, leaves Name and Type locked, read from the inputs' `disabled` state and not only from the controls'; a read-only `form.disable()` locks everything and a later `enable()` gives only the Value cell back. Each guard was deletion-checked and turned a named test red. eslint, prettier and the production (AOT) build pass; the full frontend suite passes; every statement, function and branch this PR adds is covered. Checked on a local instance (a Python source feeding a Python UDF whose code declares two UI parameters), the panel widened so the columns are in view. On main the cells are editable on most openings (4 of 5) and after every run; with this PR they stayed locked on every opening and through a forced `setInteractivity(true)` followed by real change-detection passes, read from the DOM as well as from the controls. The Value cell follows the host in both. #### Video **Before** https://github.com/user-attachments/assets/53ac4826-b5ec-4852-a5c9-22685306839d **After** https://github.com/user-attachments/assets/9bcc2a52-fdb3-4764-99cb-63a31a18fa44 ### Was this PR authored or co-authored using generative AI tooling? Yes. Generated-by: Claude Code (Anthropic). Co-authored with Claude, reviewed line by line by the author before submission. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
… hides, and add a parameter to the card's operator The ui-udf-parameters widget hides formly's labels and draws fixed column headers (Value / Name / Type), so the author's per-sub-field overrides from the Form View changed nothing a reader saw: a renamed column kept its old name and a hidden one kept its header over an empty cell. On a Form View card the widget now decides which columns are in view from the first row's sub-fields (a hidden column, or one whose group is hidden, is left out, header and cells alike), read live, since the form's hide lands on a row after formly has built it; the header shows the author's name for the column, else the panel's fixed label. The Form View names the operator on every field it renders (props.operatorID, FIELD_OPERATOR_ID_PROP / fieldOperatorID), since nothing is highlighted on the form; the same mark tells a shared widget which host it is on. Add parameter uses it: it inserts a declaration into an operator's code, and it took that operator from the canvas highlight, so on the form it edited the wrong operator or failed. It now adds to the field's operator on a card, and to the highlighted one on the panel. The declared parameters then reach the operator: the panel's frame writes them only for the operator it shows while it acts as the editor, so the form writes them when the frame does not (a writer only, the operator still on the graph). A reader's value cells stay locked: the Form View marks a reader's field props.disabled, and the widget's own "the value is editable" used to re-enable the control after formly had disabled it. And the widget's own lock on the Name and Type cells (inferred from the code; a name typed there no longer matches a declaration) now holds under any host: the panel enables its whole form group when it turns interactive, and formly mirrors a control's state into props.disabled, so a one-time disable did not survive on the canvas while the form showed the cells locked. The widget holds a locked cell through the control's disabled-change hook, which disables it again whenever a host enables it; a cell configured editable is released. Closes apache#8763 Co-Authored-By: Claude Fable 5.1 <[email protected]>
11d0214 to
b6a6c46
Compare
…der, once While authoring, the form's label wrapper decorates every row's sub-fields: the first row with the name box and the eye, every later row with a static name. The ui-udf-parameters renderer draws those sub-fields in its cells under a header of its own, which this PR already makes read the author's name -- so the name was drawn three times over: the header, the first row's box, each later row's static label (and the static one said "Attribute Type" where the header said "Type"). The wrapper gains one prop, labelHosted: a host that draws the label itself sets it on the field, and the wrapper then renders no box, eye or static label, only the control's accessible name and the dimmed state. The renderer hosts the one name box and eye per column in its header, bound to the first row's props and callbacks -- the row the form gave the controls to -- and, for each column it hosts, sets the prop on every cell of that column, with the column's own label as the accessible name so a screen reader hears what the heading says. The write-through those controls make (the props first, then the form) and the box's and eye's styles now have one definition each, on the wrapper (rename / setHidden, _editable-label.scss), which the header uses rather than copies. Nothing in the form's walk changes; a renderer that draws no header of its own (the generic array widget) sets nothing and is untouched; the operator property panel is untouched too (no operator mark, no hosting). How a later row's own name keeps up with a rename is the form's followersByPath, as before this commit. Co-Authored-By: Claude Fable 5.1 <[email protected]>
b6a6c46 to
9e26b4f
Compare
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
3 open issues
Review effort: Lite
…d say where the lock stops meaning a run Two from Copilot's review. subFieldViews takes a map; its guard let an array through to the cast, so it now refuses one as it refuses a string. And isExecutionInFlight's note said "there" for the Form View, which read as a typo; it names the page now. Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
/request-review @carloea2 |
…eady holds (apache#9408) ### What changes were proposed in this PR? Closes apache#9400. A workflow's two views share one selection: it lives in the root-provided `JointGraphWrapper`, which the switch between the canvas and the Form View hands over rather than rebuilds. Nothing is lost in the switch, but everything that shows the selection followed only the highlight streams and never read it when it mounted. The event that made the selection had already happened, so the view arriving at it waited for an event that never came: the panel showed nothing, and the step was drawn as not selected. Clicking that step did not help, since the wrapper treats an already-highlighted element as a no-op and emits nothing; it took clicking a *different* step to bring the panel back. Each of the three now reads the selection it mounts into, then follows the changes as before: - `PropertyEditorComponent` (the canvas's panel and every host of it, so not specific to the Form View, to any operator type, or to operators as opposed to ports): the body of the highlight subscription becomes `showHighlighted()`, the same decision as before (one operator, one port, or nothing) with the `syncTexeraGraph` guard that the stream applied as a `filter` now inside it, so it is applied once on both paths. `registerHighlightEventsHandler()` calls it before subscribing, and only when something is highlighted: the no-frame branch is for a selection being dropped (it clears the editing marker on the shared awareness channel and resizes the docked panel to its empty content), which a fresh mount must not do. It is also called after the toolbar's choosing toggle has replayed its value, so the frame it builds carries `choosing` from the start instead of being torn down and rebuilt by the replay inside the page's first change-detection pass. Not `startWith` on the stream: its initial emission is unconditional, which is exactly the empty-branch-at-mount this avoids. - `WorkflowEditorComponent`: the ring. The paper is new on every switch, and the highlighter was applied only from the stream, so the arriving view would have shown its panel for a step drawn as not selected. The handler now rings the operators and comment boxes the wrapper already holds, then follows the streams; the `findViewByModel` guard the unhighlight branch already had is shared with it. - `WorkflowFormComponent`: one call to the `syncSelectionFromHighlight()` it already has, in `ngOnInit` after it subscribes, so the form's step panel opens on the selection it arrives with. The template note that justified `[hidden]` over `*ngIf` by the panel having "no initial pull" is reworded to the reason that still holds. No change to who owns the selection, to what a click does, or to the canvas's own behaviour when it is the only view open: with nothing selected, nothing is read, and both panels mount empty exactly as before. The wrapper's no-op for an already-highlighted element is deliberately untouched (it is what keeps a shift-selection stable); it simply stops mattering, because the arriving view already shows that step. #### Video **Before** https://github.com/user-attachments/assets/e00affd4-46aa-4691-acc4-5d33060da766 **After** https://github.com/user-attachments/assets/f50674e4-b0fd-4941-bcca-860d1aa61abe ### Any related issues, documentation, discussions? Closes apache#9400. Part of the Form View (apache#8011). Related to apache#8846, which touches `workflow-form.component.ts` elsewhere and does not read the selection on mount: its `frameWrites` guard compares `selectedOperatorId`, which only `syncSelectionFromHighlight()` sets, so on arriving in the Form View with a step selected that guard saw `undefined` and the form wrote the declared parameters even while the live panel showed that step. With this change `selectedOperatorId` is set on arrival and the guard holds. The two do not overlap in code; whichever lands second rebases. ### How was this PR tested? - `property-editor.component.spec.ts`: a panel mounting into an existing selection shows that operator; mounting into no selection leaves the panel alone and mounting into two shows no frame (the two-operator selection is made explicit with `setMultiSelectMode(true)` and asserted before the check); and the frame a panel mounts into is built with the toolbar's choosing state, with no rebuild, which pins the order of the two subscriptions. - `workflow-editor.component.spec.ts`: wiring the highlight handler into a selection the wrapper already holds rings that operator once (a spy installed after the stream has done its work). - `workflow-form.component.spec.ts`: the form's step panel opens on the step the canvas had selected, and stays closed for none or several — seeded with a stale id, so a read that never happened could not pass as "undefined". - Existing specs for all three components pass unchanged; they cover what the streams do after mount. - The full frontend suite, eslint, prettier and the production (AOT) build pass. - By hand on a local deployment, `main` (`cfca8e1e5`) and this branch served side by side on the same backend and the same workflow, driven by a Playwright probe that reads what the panel shows at each step: | step | main | this branch | | --- | --- | --- | | click the step on the canvas | the step | the step | | switch to the Form View | empty | the step, ringed on the preview | | click that same step on the preview | empty | the step | | switch back to the canvas | empty | the step, ringed on the canvas | ### Was this PR authored or co-authored using generative AI tooling? Yes. Generated-by: Claude Code (Claude Fable 5.1, Anthropic). Co-authored with Claude, reviewed line by line by the author before submission. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <[email protected]>
…p rebuild in fewer cases The widget's spec pinned the rule for Add parameter on a card (a writer's offers it without the canvas's flag, a reader's does not, nor a writer's mid-run) six times by hand, and the formly spec pins the same rule under real formly, where a reader's lock is the one formly applied -- the only place the rule can be told from its mock. The hand-made six become two: what the widget does with the rule (adds to the card's operator; adds nothing and keeps the draft open when the card is not editable, whoever calls). The form's spec folds the catch-up rebuild's twelve cases into nine: a rebuild that cuts the old cards off and one that holds the new rows are one rebuild, the frame being the writer and the form leaving the write to it are one case, and no card showing the step is one case whether the input is another or the step is. Every line and branch the PR adds stays covered. Co-Authored-By: Claude Fable 5.1 <[email protected]>


What changes were proposed in this PR?
Closes #8763. Part of the Form View feature (#8011); from the #8439 refinements (item 6).
ui-udf-parameters(the Python UDF parameter table) is exposable, but its renderer hides formly's labels and draws fixed column headers (Value / Name / Type), so the author's per-sub-field overrides from the form (#8438) changed nothing a reader saw: formly expresses a rename as the sub-field's label and a hide ashide, and renders both through the field's wrapper, which this widget's cells never go through -- a renamed column kept its old heading, a hidden one kept its heading over an empty cell. Reviewing it turned up more the widget got wrong on a Form View card. The operator property panel is untouched throughout.common/formly/sub-field-view.ts: a host that holds per-sub-field decisions publishes them once on the field it hands a widget (props.subFieldView, keyed by sub-field path:{ label, shown }-- the author's name, or""where they kept the one it came with, and whether a reader sees the part at all), and a widget that draws its own chrome askssubFieldsInView/subFieldLabel. Reading the state back out of the rendered rows was not the answer: it is only there once formly has built a row, so a table whose UDF declares nothing yet had nothing to read, and a group's hide is marked on the group while its children are merely not rendered. The Form View fills the map during its override walk (the first node met for a path answers for it, as it does for the controls: the rows of a repeated section share one override),shownfor an author always (who keeps a hidden sub-field on screen, faded, to bring it back) and for a reader unless the override hides it; a hide of theattributegroup takes both its columns out. A name being typed is written into the very object the field carries, so a header follows the typing without a rebuild; an emptied box deletes the override and the header falls back to the widget's own name. Nothing published -- the panel, where there is no author -- means every part in view under the widget's own name, which is what its fixed headers already say.columnsis read live (the form hands the widget its field after the widget's first check) throughsubFieldsInView, which hands back the same array instance while the answer has not changed so the header'sngFordoes not redraw; a header readssubFieldLabelwith the fixed label as fallback -- the author's name, else "Type", never the schema's "Attribute Type", which the panel does not show either. All three columns come back while the draft row is open: that row names and types the parameter being added, so it needs them whatever the author hid (Copilot on this PR: a filtered header over its three cells put the name box under the wrong heading).labelHosted: a host that draws the label itself sets it, and the wrapper then renders no box, eye or static label, only the control's accessible name (sr-only, tied to the control byfor, or the group twin for a repeated field) and the dimmed state. The widget hosts the one name box and eye per column in its header, bound to the first row's props and callbacks (authorName/authorHidden/renameField/setFieldHidden-- the row the form gave the controls to), and for each column it hosts marks every cell of that column, named as the heading reads. The write-through those controls make (the props first, then the form) and the box's and eye's styles have one definition each, on the wrapper (EditableLabelWrapperComponent.rename/setHidden;_editable-label.scss), which the header uses rather than copies. How a later row's own name keeps up with a rename is the form'sfollowersByPath, as before. A renderer with no header of its own (the generic array widget) sets nothing and is untouched; the panel carries no operator mark, so nothing is hosted or marked there.props.operatorID:FIELD_OPERATOR_ID_PROP/fieldOperatorID/renderedInFormViewincustom-formly-type.ts), since nothing is highlighted on the form; the same mark tells a shared widget which host it is on, where the two must differ.editable) asks the two halves of the canvas's rule separately, because the canvas's flag answers neither one on that page. Write access: the card is not a reader's, which the form marks on the field itself (props.disabled;renderedReadOnly). So a writer's card offers it in and out of edit mode, as every value on the card can be edited, and adding a parameter is adding a row; it followed the canvas's modification flag, which the Form View holds off outside edit mode against structural edits through its page, so a writer could edit every value on the page but not add a parameter to this table without entering edit mode. No run in flight: asked of the execute service, which owns that rule (isExecutionInFlight(), lifted out of the switch that already locks the graph, so the states are listed once). Adding a parameter writes into the UDF's code, and code is what a run must not have rewritten under it. The mark decides rather than the table's own control, which does not answer the question: formly disables the leaf controls under a disabled field, not the array itself, so a reader's table with no parameters yet reads as enabled and would have offered the button. The check also sits in the method, not only on the button. The panel keeps the canvas's rule, the flag, unchanged.props.disabled, and the widget's own "the value is editable" used to re-enable the control after formly had disabled it.Any related issues, documentation, discussions?
Closes #8763. Raised by Copilot on #8438; filed from #8439. Rebased on main, which now has #8848 (the widget's lock on the Name and Type cells holding under the panel's wholesale
enable(), split out of an earlier revision of this PR) and #8845 (a card's pending write is flushed before every rebuild, which the catch-up rebuild here inherits; an earlier revision listed that as #8844, open then). #8760 (the form's own code editor host) needs the same operator mark and the same write of the declared parameters; whichever lands second drops its copy. Checked under real formly while reviewing: hiding theattributegroup itself keeps the rows' names and types in the model (formly resets a hidden field's children only on a hide that happens after the build; the form setshideat build only, and a reader's card is rebuilt rather than toggled). Table semantics for assistive tech (the header and cells are plaindivs, as on the canvas) stay a follow-up. The remaining stale-row window is a collaborator declaring a parameter while this user types in that same card, where the rebuild is held and the card's write-back puts the old list back; closing it means a card writing only its own key into the operator's current value instead of replacing the whole property, which is a change to every property's write-back and belongs in its own PR.Seen in review, the Form View's own and not changed here: the form's array widget (
ArrayTypeComponent) shows its add and remove buttons to a reader too, without a write-back behind them; that goes to a follow-up issue. And on main, renaming or hiding a repeated section's sub-field reaches only its first row at once:followersByPathregisters the configs the section's builder returns, which formly clones before rendering, so the rows on screen never hear of the edit until a rebuild shows the saved override on every row. Pre-existing, found while checking the second commit, which hosts the first row's controls and leaves that mechanism as it is; it goes to its own issue.An earlier revision of this PR let a run through. It described a writer's cards as editable mid-run and read the button as following from that, which holds for the values -- they were always editable here -- but not for this button: the canvas's flag had been hiding it during a run, and dropping the flag dropped that with it. A declaration goes into the UDF's own code, straight into the shared document and not through the graph's lock, and the property panel and the canvas code editor both refuse a code edit while a run is in flight (mengw15). The card now asks for that half on its own; see the rule above.
How was this PR tested?
Unit tests (vitest), added or extended in this PR; every statement, function and branch the PR adds is covered, with the rule for Add parameter pinned once, under real formly, rather than again by hand.
sub-field-view.spec.ts: a host that publishes nothing leaves every part in view under the widget's own name; a publishing host drops what the author hid and keeps the rest in the widget's order, gives the author's name and the widget's own where the author kept it, and keeps, and names, a part it did not mention; the same array comes back while the answer has not changed and a new one once it has, each host's answer kept apart; a published value that is not a map is ignored.FormlyModule.forRoot, a builder row template as formly-jsonschema produces,nz-inputcells as in the app, so the rows are formly's own and a reader's lock is the one formly applied): a writer's Form View card offers Add parameter without the canvas's flag and a reader's card, its field disabled, does not; the same with no parameters yet, where formly leaves the empty array control enabled; a run in flight takes the button off a writer's card and leaves the values editable.labelHosted, the row that carries the controls draws no box or eye but is still named and still fades when hidden; a later row draws no static name, only the sr-only one tied to the control byfor, or the group twin with its-labelid for a repeated field;rename/setHiddenwrite the props and then call the form, and tolerate a reader mount that has neither callback.isExecutionInFlightagainst the graph lock, state by state;custom-formly-type's spec pinsfieldOperatorID,renderedInFormViewandrenderedReadOnly.eslint, prettier and the production (AOT) build pass; the full frontend suite passes.
Checked on a flag-on instance (main-demo; a Python source feeding a Python UDF whose code declares two UI parameters,
uiParametersexposed with the Name column renamed "Parameter" and the Type column hidden), the canvas and the form open side by side, main against this branch. On main the card shows Value / Name / Type, the overrides ignored. With this PR the card shows Value / Parameter, the Type column gone, while the canvas panel still shows Value / Name / Type. A writer's card offers Add parameter in and out of edit mode (on main it appeared only in edit mode, while any list's rows could be added either way); addingcutoff(double) insertsself.UiParameter(name="cutoff", type=AttributeType.DOUBLE)into that UDF's code, and the new row appears on the card and in the canvas tab's panel within a few seconds, and is there after a reload. In edit mode, the first commit alone drew 3 header titles, 3 name boxes and 3 eyes in the first row and 3 static labels in the second; with the second, the header holds the 3 name boxes and 3 eyes and the cells hold none, and a hidden column's header fades with its cells.The canvas panel shows the same three columns, the same values and the same behaviour as on main, side by side on both instances.
Video
Before
Screen.Recording.2026-10-04.at.10.13.07.PM.mov
After
Screen.Recording.2026-10-04.at.10.13.22.PM.mov
Was this PR authored or co-authored using generative AI tooling?
Yes. Generated-by: Claude Code (Claude Fable 5.1, Anthropic). Co-authored with Claude, reviewed line by line by the author before submission.
🤖 Generated with Claude Code