Conversation
…the admin already ships? Hand-written menu-builder renderer for an object list field, using nothing the generated code would have to bring itself. Not shipping code; it exists to answer one question before any generator is designed. It answers yes, and by more than expected: `admin-ui` exports `Tree`, which already does drag, drop, nesting and collapse, so even the drag library named in the tier-1 plan is unnecessary. The reorder itself is `field.moveItem`, which the form model already exposes, so the renderer supplies presentation and calls the model. Findings are recorded at the bottom of the file, including the one that matters most for the generator: naming the libraries is not enough. Four lines of the first draft failed to compile, all plausible guesses at an API rather than nonsense. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The second half of the probe: can a string that never went through the repo's compiler become a working field renderer in the admin? - rendererRuntime.ts — the injection set (React, admin-ui, observer, the two createFieldRenderer helpers) and loadRenderer(), which evaluates a bundled IIFE and calls createRenderer(runtime). Same mechanism ComponentEditorPresenter already uses for remote components. - bundleRenderer.ts — strips imports, wraps the source in that factory, and transpiles JSX with esbuild-wasm in the browser. Nothing resolves at load time, so a generated renderer cannot reach for a dependency it was not given. - GENERATED_SOURCE.ts — the cold-generated renderer as a string, which is how it will arrive: out of a model, into the CMS, never through a build. - GeneratedRendererProbePage.tsx — builds a real FormModel, bundles, loads, and renders through FormView. Mounted in the existing FormModelDemo page. - generated/MenuBuilderFieldRenderer.tsx — what the cold agent produced, kept for the diff against the hand-written one. Everything compiles. It is NOT yet confirmed on screen: this checkout's webiny.config.tsx currently fails to render in Node on an SVG import, which reproduces with the whole tree stashed and so is not caused by any of this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hat only runtime finds The cold-generated menu builder now renders in the admin, bound to a real FormModel, with no build step for the generated code itself. Adding items, typing into them and reading back form.getData() all work. Getting there turned up two bugs that compile cleanly and only show up on screen with data in them: - Custom node data was passed to Tree flat and read back flat. Only the read side is flat. NodeDto.data goes in nested and WithDefaultNodeData<T> comes back merged. - No sort prop was passed, so Tree fell through to react-dnd-treeview's default and alphabetized the rows by label. A menu builder whose whole feature is drag-to-reorder silently reordered itself as soon as a label was typed. Nothing throws, and the form data stays correct. That second one is the interesting result: a compile-and-retry loop would never have caught it. Findings are written up at the bottom of MenuBuilderFieldRenderer.tsx. Also fixes the bundler hanging forever. The wasmURL pinned 0.28.1 while the installed package is 0.28.2, and a version mismatch throws inside the worker where nothing rejects the promise. It now takes the version from the package, and memoizes the init so a second mount can't call initialize twice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… then runs
Closes the loop end to end. You type "build me a menuBuilder renderer" into
the command palette, approve the tool call, reload, and the field renders
through code nobody committed.
API:
- A private CMS model, wbyAdminComponent, holding the source. Its own model
rather than a corner of the website builder's, since the two share nothing
but the word "component".
- createFieldRenderer, an AiSdkToolDefinition. Needs approval, and for a
stronger reason than the other write tools: it stores code that later runs
in an admin user's browser, so the approval screen is the code review.
- listAdminComponents(kind) for the admin to read back. Newest wins, one per
name, because asking for a fix writes a new entry rather than editing the
old one and the stale version would otherwise take the name back.
Admin:
- app-admin gains a real home for the loader that was sitting in probe/, plus
GeneratedFieldRenderer, which bundles one stored source and registers it
under the name fields ask for. Registration waits for the bundle so a
half-loaded renderer never claims a name.
- ai-powerups fetches on boot and registers whatever is stored.
The interesting part is the tool description. Told in prose to pass
sort={false} and to nest node data, the model wrote tree={...} and
render={...} instead of nodes/renderer: the prop names of
@minoru/react-dnd-treeview, which admin-ui's Tree wraps but does not
re-expose. JSX takes unknown props without complaint, so it compiled, loaded,
registered, and drew an empty box. Giving it the literal signature instead of
a description fixed it on the next attempt.
Which is the finding worth keeping: the contract has to name props rather than
describe them, and a wrapper over a well-known library is where a model will
confidently reach for the wrong API. Notes are at the bottom of
MenuBuilderFieldRenderer.tsx.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A generated renderer needs two registrations to be usable and only one was
there. AdminConfig.Form.FieldRenderer resolves a name to a component when a
form renders; CmsFieldRenderer is the catalogue the field editor's Appearance
tab offers. With only the first, a renderer worked but nothing could ever
select it, because that dropdown is the only way to set a field's renderer
name.
createFieldRenderer now also takes label, fieldType and appliesTo, so canUse
can keep a menu builder off a number field, and the renderer list has a short
title with a real description under it rather than the same sentence twice.
Also fixes the bundler: it used esbuild's jsx loader while every label on the
input said TSX, so the first type annotation the model wrote was a syntax
error. Types are erased, never checked.
Verified in a real entry form: a red text input and a live character counter
that turns orange past 20 characters, both picked from the Appearance tab.
Three more wrong-API bugs turned up on the way, and by now the pattern matters
more than the bugs. <Text text={...} /> when Text takes children and only
Button and FormComponentLabel take text. Iterating field.validation looking
for rules when it is a { isValid, message } result. Each was a plausible guess
that React or the form model accepted silently. Adding each one to the prose
contract fixed that bug and not the class, and the contract is now most of a
hand-written type definition. Generating it from the real types is the way
out. Notes are at the bottom of MenuBuilderFieldRenderer.tsx.
Known gap worth a look in review: CmsFormModelBuilder is a singleton that
reads the renderer list once, so a renderer registered after the first entry
form resolves it is left out of the map, and applyFieldProps then skips
builder.renderer() silently. The boot fetch wins in practice because reaching
an entry form takes a navigation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prose contract was wrong three times in a row, and every time the right
answer was already sitting in a .d.ts. So it is generated now.
A script reads the built types of admin-ui and app-admin with ts-morph and
emits the component props, the size unions, the Tree node shapes and the field
view model. The hand-written half keeps only what no type can state: that
sort={false} is effectively mandatory, that a renderer must not hold field data
in local state, that a guessed prop name fails silently rather than throwing.
What makes it readable is dropping anything declared inside @types/react.
InputProps resolves to 309 properties, 292 of them inherited DOM attributes.
The 17 that remain are the ones a caller actually chooses. children is the one
deliberate exception, since whether a component takes children is exactly what
went wrong before.
The next renderer generated after the change, a drag-to-reorder checklist asked
for cold, got every earlier bug right first time and needed no correction
round: nodes and renderer rather than the wrapped library's tree and render,
sort={false}, string ids, custom data nested going in and flat coming back,
aria-label on IconButton, and both <Text>children</Text> and
<FormComponentLabel text={...} /> correct in the same file. That last pair is
the distinction it had generalised away twice.
One sample is not a measurement, and the generator cannot help with anything
that is a judgement rather than a shape. But the failures it removes were the
entire correction loop up to now.
Regenerate with:
yarn workspace @webiny/ai-powerups generate-renderer-contract
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnN5e1pnoEBwkKshTrETs6
…and for the assistant A developer writing a field renderer by hand and the assistant writing one from a prompt need the same knowledge. Until now they had two copies: a 772-line form-model skill served over MCP, and a contract I hand-wrote for the tool description, which was wrong three times. Each extension kind is now authored once in skills/shared/extensions/ with @shared, @dev and @admin sections, and scripts/generateExtensionContracts.mjs renders both targets: - skills/user-skills/admin/extensions/<kind>/SKILL.md, shipped in @webiny/mcp by its prepublishOnly copy. Gets @shared + @dev: imports, file paths, AdminConfig.Form.FieldRenderer and CmsFieldRenderer registration. - rendererContract.generated.ts in ai-powerups. Gets @admin first, since the sandbox rules override anything the shared half implies, then @shared. The sections differ only by execution environment, which is the whole reason they cannot be one undifferentiated file: a developer imports freely and has a compiler, the assistant has neither. Sharing the file verbatim would teach the assistant to write import statements. Both bake a copy at build time from the one repo source, so there is no runtime coupling between two published packages, and a project gets the guidance that shipped with its own runtime. Also closes the drift I introduced last commit. The injected component list lived in bundleRenderer.ts and again in the generator's hardcoded array. bundleRenderer now exports INJECTED_COMPONENTS as the single definition, the wrapper builds its destructure from it, and the generator reads it from source. Verified by adding a component to the list and watching it appear in the contract. A component can no longer be described as available without being in scope, or vice versa. One command regenerates everything: yarn generate-skills now runs the existing skills generator and this one. Regenerating also produces a 43-file whitespace-and-frontmatter diff across skills/user-skills/generated/, which is pre-existing drift between the committed tree and what the generator emits today. Left out of this commit so it stays reviewable. Worth a look on its own, since it tags admin/configs with context: webiny-api. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnN5e1pnoEBwkKshTrETs6
…xtension kind Menus prove the shared-source structure generalises, and they are the cheap case on purpose: a menu is configuration, not code. Nothing is bundled, nothing is evaluated, and GeneratedMenus maps stored rows onto the same AdminConfig.Menu an extension would use, so the sidebar cannot tell one of these from one written by hand. That is the argument for sorting extension kinds by whether they genuinely need arbitrary code. This one needed none of the machinery the field renderer does: no esbuild, no new Function, no contract about which components exist, and no way to fail at render time beyond pointing at a path that does not resolve. The stored JSON is still parsed defensively, because a hand-edited CMS entry is the one route that bypasses the tool's validation and a malformed row must not take the sidebar down. Authored in skills/shared/extensions/menu.md like the field renderer, so the MCP skill and the tool description come from one source. Its @admin section is short, which is the shape you want: the sandbox has little to say when there is no sandbox. Verified end to end: asked for a Reports item under Settings, approved, and it appears in the sidebar and navigates. One thing to know if you touch this file: `~/admin/...` in ai-powerups' admin/Extension.tsx does not get rewritten to a relative path on build for a newly added directory, and the admin app then fails to resolve it. The neighbouring imports use the same alias and work, so this looks like stale build state rather than a rule. Switched to a relative import; worth a look if it bites someone else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnN5e1pnoEBwkKshTrETs6
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueComment |
|
🚓 Slop Cop Coherent, well-documented experimental PR that matches its stated (self-disclosed WIP) scope, but ships probe/demo code and an unsandboxed new Function() eval path into a published package, both already acknowledged by the author as gaps to fix before merging. 🚨 Should this be in the PR? 🔴 High — PR explicitly self-flagged as experimental/WIP, not a merge candidate The PR title and description clearly state this is an experimental prototype ('Do not merge as-is') containing probe files, no tests, and multiple known unresolved gaps (no disable mechanism, singleton race condition, no environment promotion, approval-gate trust assumption). This is intentional and disclosed, but flagging it here per the review's mandate to catch content the author would want to know about before merging — the author already knows and says so. 🟠 Medium — Probe/demo code shipped inside the published app-admin package packages/app-admin/src/probe/GeneratedRendererProbePage.tsx and packages/app-admin/src/probe/MenuBuilderFieldRenderer.tsx (278 lines combined) are exploration/demo code wired into packages/app-admin/src/features/formModel/demo/FormModelDemo.tsx and mounted at a real route (/_/form-model-demo). The PR description itself acknowledges these must come out before any real merge, but they are present in this diff and increase the published package's footprint with non-production code, including a long stream-of-consciousness comment block in MenuBuilderFieldRenderer.tsx documenting the author's experimentation process (not doc comments). 🟡 Low — Unexplained workaround left in code with unresolved root cause The PR description notes ' 📏 Code-style rule checks 🟠 Medium — new Function() usage for evaluating generated code packages/app-admin/src/features/generatedComponents/rendererRuntime.ts uses 🟡 Low — Multi-line block comment not using '/ ... /' consistently ending with period In packages/ai-powerups/src/api/features/AdminComponents/menuContract.generated.ts and rendererContract.generated.ts, the generated file header comments use Automated, non-blocking heads-up from an LLM. It can be wrong — use your judgment. Regenerates on every push. |
Before this, "create a char count renderer and put it on Article's title" ended with the renderer written and the user still doing the last two steps by hand: reload, then pick it under Appearance and save the model. setFieldRenderer closes that gap, so the whole sentence completes. Kept separate from createFieldRenderer rather than folded into it. Writing code and changing a content model are two different decisions and the approval screen should show them as two, so someone can take the renderer and still refuse to have it applied to that particular field. It also lets an existing renderer be reused elsewhere without regenerating. It checks what it can. A renderer's stored fieldType and appliesTo are matched against the target field, because a renderer reading field.items on a single-value field renders nothing rather than failing. A name that is not a generated renderer is allowed through, since that is how you put a field back to a built-in, but the response says it was not checked. A code-defined model gets a specific message rather than a raw error, because "Cannot update model defined via code" is not something the user can fix from the admin and the next question is always why. Verified with the one-shot ask: it ran createFieldRenderer, then proposed setFieldRenderer as a second approval, and the field renders "0 characters" in a real entry form. Also fences the generated type reference. yarn format was stripping the leading indentation out of the markdown, which flattened the nesting and made every regenerate-then-format cycle produce a 200-line diff. Fenced blocks are left alone, and the cycle is now idempotent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What this explores
Whether a user can change their admin UI by asking for it, instead of writing an extension, pushing
it, and waiting for a deploy. Everything here is an extension Webiny already supports in code; the
question is whether the assistant can write one and the admin can run it.
Two kinds are working end to end: custom field renderers and sidebar menu items.
What actually works, verified in a browser
Typing "build me a menuBuilder renderer" into the command palette, approving the tool call, and
reloading gives a working drag-to-reorder field renderer bound to a real form. The renderer appears
in the CMS field editor's Appearance tab, can be picked per field, and renders in a real entry form.
A red text input and a live character counter were both built this way and confirmed rendering.
Asking for a sidebar item under Settings produces one that appears and navigates.
No file was touched, nothing committed, no rebuild.
How it fits together
Storage. A private CMS model,
wbyAdminComponent, with akinddiscriminator. Field renderersstore TSX; menus store JSON.
Writing.
createFieldRendererandcreateMenuareAiSdkToolDefinitions, both behind theapproval gate.
Running.
app-admingainsfeatures/generatedComponents/: bundle the stored source withesbuild-wasm in the browser, evaluate it with
new Functionagainst injected dependencies, andregister it.
ai-powerupsfetches on boot and registers both kinds.Guidance. Each extension kind is authored once in
skills/shared/extensions/and rendered twoways: the MCP skill a developer gets, and the tool description the assistant gets. They differ only
by execution environment, since a developer imports freely and has a compiler while the assistant
has neither.
The finding worth your time
Generation quality was the only thing that ever failed. The mechanism worked first time, every time.
Six bugs across four attempts, and every one was a plausible guess at an API that nothing rejected
until it was on screen.
<Text text={...} />whenTexttakes children, whileButtonandFormComponentLabeldo taketext.Treegiventree/render, the prop names of the library itwraps but does not re-expose.
sortomitted, so a drag-to-reorder list silently alphabetized itselfthe moment someone typed a label.
field.validationiterated as if it were a list of rules.Three rounds of adding the missing fact to a prose contract fixed each bug and none of the class. So
the contract is generated from the real
.d.tsfiles now, with anything declared inside@types/reactfiltered out, which takesInputPropsfrom 309 properties to the 17 a calleractually chooses. The next renderer generated after that change got every earlier bug right on the
first attempt with no correction round.
One sample is not a measurement. But the failures it removes were the entire correction loop.
Known gaps
app-admin.src/probe/is exploration and is mounted at/_/form-model-demo. It has to come out, or move to the project'sextensions/, before any realmerge. The findings comment in
MenuBuilderFieldRenderer.tsxis the design rationale and shouldmove somewhere it stays useful.
enabledfield and nothing reads it. A badrenderer can only be removed by editing the CMS entry directly.
CmsFormModelBuilderis a singleton that reads the renderer list once. A renderer registeredafter the first entry form resolves is dropped silently, because
applyFieldPropsskipsbuilder.renderer()on a miss rather than erroring.This is the question I would expect a customer to ask first.
idea is aimed at.
~/admin/...imports in ai-powerups'admin/Extension.tsxwere not rewritten to relativepaths on build for a newly added directory, while neighbouring identical imports were. Worked
around with a relative import; the cause is unexplained.
Trying it
Ask the assistant in the command palette for a field renderer or a menu item, approve the call, and
reload.
packages/app-admin/src/probe/GeneratedRendererProbePage.tsxat/_/form-model-demohas afield asking for a
menuBuilderrenderer that nobody has written yet.Regenerate the guidance and type reference with
yarn generate-skills.🤖 Generated with Claude Code
https://claude.ai/code/session_01SnN5e1pnoEBwkKshTrETs6