ADR 0005 — P2: fill_form, hover, and the select_option drop

  • Status: Accepted
  • Shipped: v3.0.0
  • Sources: docs/superpowers/plans/2026-04-27-p2-fill-form-hover.md (deleted in commit 20c2e14; recoverable from git history)

Context

P2 of the Playwright-parity roadmap originally listed three tools: fill_form, select_option, and hover.

After P0 wait_for shipped, the select_option shape became questionable — wait_for already returned a fresh snapshot in its payload, which collapsed the round-trip math select_option was supposed to save.

Decision

Drop select_option. Same logic that killed handle_dialog accept in P1: post-wait_for, the equivalent flow is

tap_widget(dropdown)
  → wait_for(text='option')   // returns the fresh snapshot
  → tap_widget(item)

Three tool calls. A wrapper saves zero round-trips because wait_for already returns the snapshot. And a select_option implementation would need Flutter-version-specific dropdown logic (DropdownMenu vs. DropdownButton vs. custom popup pickers) that's not worth the maintenance surface for zero functional gain.

This brought P2 from 3 tools / 6 tasks to 2 tools / 5 tasks — back to the "small batch" the roadmap originally promised.

Ship fill_form and hover — both follow the wait_for blueprint for registration.

fill_form — server-side orchestration

_fillForm is a server-side loop. Each field calls the existing _enterText executor (which itself does _ensureVmConnected + callFlutterExtension('ext.mcp.toolkit.enter_text', ...)). No new toolkit code, no new wire format, no OnFillFormEntry. Reuses tested code.

ConcernDecision
Failure semanticsStop on first failure. Return {success, results, failedAt, failedRef}. Partial form is worse than a clean error pointing at the bad field.
Snapshot stalenessOptional snapshotId for the whole batch. If the first field returns stale_snapshot, fail the batch. Don't re-validate per field.
Connection check overheadHappens once per _enterText call. Fast-path is cheap (already-connected returns immediately) — not worth refactoring.

hover — synthesized pointer-hover event

hoverAtRef(ref) lives on GestureInteractionService. Synthesizes a PointerHoverEvent at the widget's center via the existing tier-2 GestureBinding.instance.handlePointerEvent path (same path as _dispatchTap etc.). No semantic-action equivalent for hover, so no tier-1 fallback.

Mouse-tracker priming gotcha. MouseTracker computes MouseRegion.onEnter/onExit from position changes. A single PointerHoverEvent at the target may not trigger onEnter if the tracker's last-known position is already over the widget (or unset). The implementation fires a priming hover at Offset(-100, -100) first, then the target hover. Reuses pointer: 1 for both events so the tracker treats them as the same logical mouse.

No new app-binding API

Unlike P1's setNavigatorKey, neither fill_form nor hover requires app-side opt-in.

Wire format asymmetry

  • Server _fillForm does not call an extension RPC — it loops _enterText directly. So no toolkit-side OnFillFormEntry and no mcpToolkitExtKeys.fillForm constant.
  • Server _hover calls mcpToolkitExtKeys.hover with {ref} (string, no encoding needed).

Consequences

What changed:

  • Two new MCP tools shipped (fill_form, hover).
  • New error codes: fillFormFailed, hoverFailed.
  • fill_form becomes the canonical pattern for "batch a server-side loop over an existing per-item executor" — no toolkit-side change needed when the per-item already exists.
  • select_option officially deferred (see ADR 0002 for the deferral list).

What we paid:

  • The hover priming hack is fragile-looking but verifiable: the test asserts MouseRegion.onEnter fires. If a future Flutter version changes the MouseTracker semantics, the test will catch it.
  • Agents asking for select_option will get tool_not_found and have to learn the tap → wait_for → tap idiom. Documented in the audit row.

Notes

Why hover doesn't have a semantic-action tier. tap_widget falls back through three tiers (semantics action → gesture detector → pointer event) because each tier has a different reach. Hover has no semantics-action equivalent in Flutter — MouseRegion only listens at the pointer-event layer. Hence the single tier-2 path.

Test pattern. Both tools use testWidgets with the act-then-pump pattern (no parallel-pump needed — neither awaits user-time delays internally). The hover test asserts a MouseRegion.onEnter callback fired.

Related work

  • Re-evaluation triggers for the deferred consolidations (including select_option if usage data ever justifies it) live in ADR 0002.