You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
forms: fixes and improvements for the forms inspector #348
Fixes and improvements from a code audit of the forms inspector, done in October 2026. Roadmap work lives in #340, #341, #342 and #343. Items marked (to confirm) were not reproduced; check them before fixing.
Problems
Medium
Full-document rescan on almost every push: the MutationObserver sets hostsStale on any childList change anywhere, so the next push runs document.querySelectorAll('*') plus getDirectives on every element. Pushes are scheduled on input and change, so a page with a clock, live list or toast rescans the DOM on each keystroke. Ignore mutations with no added or removed elements, merge added subtrees into hosts instead of a full rescan, and throttle full rescans to the heartbeat. highlight-form-field and ActionContext.all should use the cached hosts too (packages/devtools/src/forms-collector.ts:127-133, :370-373, :652, :699).
Forms in shadow roots are never found:findForms gets document.querySelectorAll('*'), which does not enter shadow roots, and no forms*.ts file handles shadowRoot. Forms in ViewEncapsulation.ShadowDom components are invisible to the panel and agents. Walk open shadow roots with a TreeWalker (or reuse the component tree's host list), and add a ShadowDom form to the demo app with a test (packages/devtools/src/forms-collector.ts:372).
Form ids are not reused after a form is recreated: in createFormIds, used only grows and ids are keyed by root identity, so revisiting a route gives Checkout.form~2@page, then ~3. Panel selection, form history filtering, snapshots and ids stored by agents break after one navigation round trip. Release ids of roots that are no longer live on each push, and keep ~n only for forms on the page at the same time (packages/devtools/src/forms.ts:775-790).
Form actions without formId act on the first form found:runFormAction falls back to ctx.forms.values().next().value, and the page handler runs a request with no formId on every page that has any form. request-form-action accepts such requests. Require formId for every action except locate, pick, cancel-pick and instrument, and reject it in requestFormAction when missing (packages/devtools/src/forms-actions.ts:677, packages/devtools/src/forms-collector.ts:684, packages/devtools/src/devframe.ts:1111-1124).
Low
A running WebMCP call tags every event as agent:originNow ranks webMcp.active() above user activity, and running drops only when the promise returned by execute settles. User typing and code changes during a long async submit are tagged agent. Count only the synchronous window (current !== null), or tag only the form linked through webMcp.link (packages/devtools/src/forms-collector.ts:162-163, packages/devtools/src/forms-webmcp.ts:264-293).
modelContext is looked up only once:watchWebMcp reads modelContextOf() once at overlay load. A polyfill or extension that adds modelContext later is never patched, and provided without modelContext can be reported wrongly. When context is null, look again in describe() and patch then (packages/devtools/src/forms-webmcp.ts:239, :449).
WebMCP schema inference fails silently at the depth limit:inferShape returns { blocking: [] } at MAX_DEPTH, so the parent has no schema and no reason, and the panel only says the schema could not be inferred. Arrays are inferred from value[0] only. Return a blocking entry such as nested deeper than 10 levels, and check every array item or say only the first was checked (packages/devtools/src/forms-webmcp.ts:96-103).
Unlinked WebMCP tools are not shown in the panel:FormsWebMcp renders only the linked tool() or the no-modelContext note. Page tools not linked to a form appear only in agent text. Add a page-level list that reuses the tool block markup (app/src/pages/forms-webmcp.ts:17, :107, app/src/pages/forms-inspector.ts:1341-1343).
User-mode writes to date and time inputs fail:nativeWrite sets element.value = String(value), so a Date or ISO timestamp on a type=date input becomes an invalid string and the field is cleared. Format to the input's wire format (or use valueAsNumber) for date, datetime-local, time, month and week, and refuse values that can't be parsed (packages/devtools/src/forms-actions.ts:419).
Page-changing actions bypass the write gate:instrument patches form prototypes and store-as-global overwrites window.$form and window.$control, but neither is in FORM_WRITE_ACTIONS, so both run with actions.forms: false. Document instrument as read-only by design or gate it, and refuse or warn in store-as-global when $form already exists and was not set by the devtools (packages/devtools/src/config.ts:387-401, packages/devtools/src/forms-actions.ts:882-891).
changed can miss edits:changedOf compares serializeFormValue output, which clips strings at 200 chars, objects at 20 keys and depth 3. An edit past those limits reports changed: false. Compare an unclipped hash of the raw value instead (packages/devtools/src/forms.ts:164-167, :214-221).
The forms page is one 1,666-line component: it mixes list, detail, action bar, picking, tabs and loading, against the "keep components small" rule. Extract FormsList, FormsActionBar and FormsTree components and a FormsStore service (app/src/pages/forms-inspector.ts).
Improvements
Inline editing of a field's value, touched and dirty state in the tree, using the existing set-value with coercion (app/src/pages/forms-inspector.ts, packages/devtools/src/forms-actions.ts).
Turn a form history slice (user value events plus submit) into a Playwright or Testing Library test, extending the export-form fixture format (packages/devtools/src/rpc/forms-cap.ts).
Signal Forms schema view: show the schema() rules per field (required, min, validate, validateHttp, disabled, hidden) and which rule produced each error or state, from the existing rule counts (packages/devtools/src/forms.ts, app/src/pages/forms-inspector.ts).
Accessibility checks in forms lint: inputs without a label, invalid fields without aria-invalid or aria-describedby, error text not linked to its input, required without required or aria-required (packages/devtools/src/rpc/forms-lint.ts, packages/devtools/src/forms-dom.ts).
WebMCP: a "Try this tool" button that calls execute with editable JSON args (gated by actions.forms), and show the exact inputSchema JSON the agent sees (app/src/pages/forms-webmcp.ts, packages/devtools/src/forms-webmcp.ts).
Skip the full serialization when no form root signal or control changed since the last push, using Signal Forms version counters and control.events (packages/devtools/src/forms-collector.ts, packages/devtools/src/forms.ts).
Per-form origin attribution: userSincePush, devtoolsSincePush and agent activity are page-wide, so a code update on form B while typing in form A is tagged user (packages/devtools/src/forms-collector.ts).
Show form setup errors (NG01xxx, setupErrors) as a page-level banner in the panel; today only agents see them (packages/devtools/src/forms-collector.ts:146, :630, app/src/pages/forms-inspector.ts).
Context
Fixes and improvements from a code audit of the forms inspector, done in October 2026. Roadmap work lives in #340, #341, #342 and #343. Items marked (to confirm) were not reproduced; check them before fixing.
Problems
Medium
MutationObserversetshostsStaleon anychildListchange anywhere, so the next push runsdocument.querySelectorAll('*')plusgetDirectiveson every element. Pushes are scheduled oninputandchange, so a page with a clock, live list or toast rescans the DOM on each keystroke. Ignore mutations with no added or removed elements, merge added subtrees intohostsinstead of a full rescan, and throttle full rescans to the heartbeat.highlight-form-fieldandActionContext.allshould use the cached hosts too (packages/devtools/src/forms-collector.ts:127-133,:370-373,:652,:699).findFormsgetsdocument.querySelectorAll('*'), which does not enter shadow roots, and noforms*.tsfile handlesshadowRoot. Forms inViewEncapsulation.ShadowDomcomponents are invisible to the panel and agents. Walk open shadow roots with aTreeWalker(or reuse the component tree's host list), and add a ShadowDom form to the demo app with a test (packages/devtools/src/forms-collector.ts:372).createFormIds,usedonly grows and ids are keyed by root identity, so revisiting a route givesCheckout.form~2@page, then~3. Panel selection, form history filtering, snapshots and ids stored by agents break after one navigation round trip. Release ids of roots that are no longer live on each push, and keep~nonly for forms on the page at the same time (packages/devtools/src/forms.ts:775-790).formIdact on the first form found:runFormActionfalls back toctx.forms.values().next().value, and the page handler runs a request with noformIdon every page that has any form.request-form-actionaccepts such requests. RequireformIdfor every action exceptlocate,pick,cancel-pickandinstrument, and reject it inrequestFormActionwhen missing (packages/devtools/src/forms-actions.ts:677,packages/devtools/src/forms-collector.ts:684,packages/devtools/src/devframe.ts:1111-1124).Low
agent:originNowrankswebMcp.active()above user activity, andrunningdrops only when the promise returned byexecutesettles. User typing and code changes during a long async submit are taggedagent. Count only the synchronous window (current !== null), or tag only the form linked throughwebMcp.link(packages/devtools/src/forms-collector.ts:162-163,packages/devtools/src/forms-webmcp.ts:264-293).modelContextis looked up only once:watchWebMcpreadsmodelContextOf()once at overlay load. A polyfill or extension that addsmodelContextlater is never patched, andprovidedwithoutmodelContextcan be reported wrongly. Whencontextis null, look again indescribe()and patch then (packages/devtools/src/forms-webmcp.ts:239,:449).inferShapereturns{ blocking: [] }atMAX_DEPTH, so the parent has no schema and no reason, and the panel only says the schema could not be inferred. Arrays are inferred fromvalue[0]only. Return a blocking entry such asnested deeper than 10 levels, and check every array item or say only the first was checked (packages/devtools/src/forms-webmcp.ts:96-103).FormsWebMcprenders only the linkedtool()or the no-modelContextnote. Page tools not linked to a form appear only in agent text. Add a page-level list that reuses the tool block markup (app/src/pages/forms-webmcp.ts:17,:107,app/src/pages/forms-inspector.ts:1341-1343).nativeWritesetselement.value = String(value), so aDateor ISO timestamp on atype=dateinput becomes an invalid string and the field is cleared. Format to the input's wire format (or usevalueAsNumber) fordate,datetime-local,time,monthandweek, and refuse values that can't be parsed (packages/devtools/src/forms-actions.ts:419).instrumentpatches form prototypes andstore-as-globaloverwriteswindow.$formandwindow.$control, but neither is inFORM_WRITE_ACTIONS, so both run withactions.forms: false. Documentinstrumentas read-only by design or gate it, and refuse or warn instore-as-globalwhen$formalready exists and was not set by the devtools (packages/devtools/src/config.ts:387-401,packages/devtools/src/forms-actions.ts:882-891).changedcan miss edits:changedOfcomparesserializeFormValueoutput, which clips strings at 200 chars, objects at 20 keys and depth 3. An edit past those limits reportschanged: false. Compare an unclipped hash of the raw value instead (packages/devtools/src/forms.ts:164-167,:214-221).FormsList,FormsActionBarandFormsTreecomponents and aFormsStoreservice (app/src/pages/forms-inspector.ts).Improvements
set-valuewith coercion (app/src/pages/forms-inspector.ts,packages/devtools/src/forms-actions.ts).export-formfixture format (packages/devtools/src/rpc/forms-cap.ts).schema()rules per field (required,min,validate,validateHttp,disabled,hidden) and which rule produced each error or state, from the existing rule counts (packages/devtools/src/forms.ts,app/src/pages/forms-inspector.ts).aria-invalidoraria-describedby, error text not linked to its input,requiredwithoutrequiredoraria-required(packages/devtools/src/rpc/forms-lint.ts,packages/devtools/src/forms-dom.ts).executewith editable JSON args (gated byactions.forms), and show the exactinputSchemaJSON the agent sees (app/src/pages/forms-webmcp.ts,packages/devtools/src/forms-webmcp.ts).control.events(packages/devtools/src/forms-collector.ts,packages/devtools/src/forms.ts).userSincePush,devtoolsSincePushand agent activity are page-wide, so a code update on form B while typing in form A is taggeduser(packages/devtools/src/forms-collector.ts).setupErrors) as a page-level banner in the panel; today only agents see them (packages/devtools/src/forms-collector.ts:146,:630,app/src/pages/forms-inspector.ts).Already tracked elsewhere
pagewhen several pages are connected: phase 1 of 4: safe by default and wired into your workflow #340.Generated by Claude Code