Skip to content

forms: fixes and improvements for the forms inspector #348

Description

@erkamyaman

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 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).

Already tracked elsewhere


Generated by Claude Code

Activity

  1. added
    bugSomething is broken or shows wrong data
    featureA feature request or a pull request that adds one
    on Oct 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or shows wrong datafeatureA feature request or a pull request that adds one

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions