Repository navigation
forms: fixes and improvements for the forms inspector #348
Copy link
Copy link
Closed
Labels
bugSomething is broken or shows wrong dataSomething is broken or shows wrong datafeatureA feature request or a pull request that adds oneA feature request or a pull request that adds one
Description
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
- Full-document rescan on almost every push: the
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). - Forms in shadow roots are never found:
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). - Form ids are not reused after a form is recreated: in
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). - Form actions without
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
- A running WebMCP call tags every event as
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). - WebMCP schema inference fails silently at the depth limit:
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). - Unlinked WebMCP tools are not shown in the panel:
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). - User-mode writes to date and time inputs fail:
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). - Page-changing actions bypass the write gate:
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). - 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,FormsActionBarandFormsTreecomponents and aFormsStoreservice (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-valuewith 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-formfixture 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-invalidoraria-describedby, error text not linked to its input,requiredwithoutrequiredoraria-required(packages/devtools/src/rpc/forms-lint.ts,packages/devtools/src/forms-dom.ts). - WebMCP: a "Try this tool" button that calls
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). - 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,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). - 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).
Already tracked elsewhere
- Refusing a form action without
pagewhen several pages are connected: phase 1 of 4: safe by default and wired into your workflow #340. - Form events on one shared timeline: phase 3 of 4: a shell you live in #342.
- Open in editor for forms, fields and validators: phase 1 of 4: safe by default and wired into your workflow #340.
- Pushing form deltas instead of full snapshots: phase 3 of 4: a shell you live in #342.
Generated by Claude Code
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugSomething is broken or shows wrong dataSomething is broken or shows wrong datafeatureA feature request or a pull request that adds oneA feature request or a pull request that adds one
Activity
fix: audit findings for store, forms, router and Analog (#362)