fix(input): reliable synthetic pointer engine — single activation, timestamps, drag hold, hittable flag, screenshot geometry - #59
Merged
charliewwdev merged 4 commits intoAug 21, 2026
Conversation
…vices
hot_reload called the VM's raw reloadSources RPC. A Flutter app embeds no
kernel compiler, so the VM answers success:false ("Error while starting
Kernel isolate task"); the report was ignored and the tool reported success
while nothing recompiled. hot_restart was the same call (or threw).
The flutter run process registers reloadSources/hotRestart as VM services
(alias "Flutter Tools") - what DevTools and IDEs call. The driver now
tracks registrations from the Service stream and invokes sN.reloadSources /
sN.hotRestart; without an attached tool it checks the ReloadReport and
surfaces a clear error instead of a fake success.
Also:
- re-bind to the new main isolate after a restart (old id -> Sentinel
Collected on every later call) and self-heal on stale isolates
- wait for ext.flutter.didSendFirstFrameEvent so the tree is populated
- on Windows, hold until the wall-clock second rolls over: Dart truncates
mtimes to seconds there, so the tool ignores edits made in the same
second as its last compile ("0 updated files")
- report what happened: "Hot reload performed by Flutter Tools (330ms)"
…g hold; screenshot geometry; hittable flag All emulated input now goes through one engine that stamps every event with a monotonically increasing timeStamp (Flutter's velocity tracker and scroll physics need them - without them a swipe on a PageView/TabBarView snaps back or is left mid-transition), always releases the pointer, and returns only after the frame that reflects the input has rendered (state queries right after an action are no longer stale; fixed sleeps removed). Frame waits are bounded: a minimized/hidden window renders no frames on Windows, which used to hang actions and screenshots for minutes; actions now return promptly and carry a 'warning' explaining the stalled frames. tap(key/text/ref) dispatched the pointer AND invoked the widget callback directly, activating twice (toggles cancelled out). The direct call is now only used when the element is not reachable by hit test at its center. inspect / inspect_interactive report 'hittable' (hit test at the element's center): 'visible' only meant 'has a size' and was true for cards hidden under a bottom bar or scrolled off screen. drag / swipe_coordinates accept hold_ms so LongPressDraggable and long-press-to-select lists recognise the drag before the pointer moves; drag also takes start/end coordinates (numbers or numeric strings). screenshot returns image_size, logical_size, device_pixel_ratio, scale and a hint: tools take logical coordinates, not image pixels. Indicator particle painter clamped its opacity (asserted every frame).
…, lazy scroll_to, scroll direction, batch settle
Sweep of the remaining tool families against a probe app (54 checks,
tools/harness_sweep.py in the linked repro):
- get_current_route/get_navigation_stack: the SDK tested
`element.widget is ModalRoute` (a Route is never a Widget) so it always
returned null, and the server treated the null as "tool not handled"
("Unknown tool"). Routes are now collected from each route's
_ModalScopeStatus scope in stack order, with name/key/isCurrent/depth;
the current route is the topmost current one (nested navigators win).
- press_key: fed KeyData to KeyEventManager, which on desktop waits for the
paired raw-key message that never comes, and mapped every physical key to
Enter. Keys now go through HardwareKeyboard + the KeyMessage handler
(Focus -> Shortcuts/Actions), with correct USB-HID physical keys and held
modifiers; Enter on a focused field also performs the "done" action.
- New Flutter implementations for tools that were CDP-only: type_text
(IME-style insert at the selection), focus, blur, set_checkbox (also
Checkbox/SwitchListTile), hover (one synthetic mouse device).
- scroll_to: pages through every scrollable when the target is not built
yet (ListView.builder), then ensureVisible.
- scroll_until_visible: 'direction' is the scroll direction, so the finger
swipe is the opposite way; finishes with ensureVisible (being in the
cache extent is not being visible).
- execute_batch: 250 ms settle between steps (step_delay_ms).
- Null results from handlers no longer surface as "Unknown tool".
Contributor
Author
|
Pushed a second commit (7e911fe) from a sweep of the remaining tool families against the probe app (54 checks, all passing): real |
… a modifier is held The tool handler dropped 'modifiers', so Ctrl+A arrived as a plain 'a' (and was inserted as text). Modifiers now reach the SDK, and while one is held the key event carries no character, as real platforms do — Ctrl+A selects all, and type_text then replaces the selection.
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Emulated input is unreliable in ways that make agent-driven testing untrustworthy. Reproduced on Windows (Flutter 3.44.9) with a probe app whose state is mirrored into a
Text, so every check below is asserted against the app's own state, not screenshots (tools/harness_interaction.pyin the linked repro):tap(key|text|ref)dispatches a synthetic pointer tap and then invokes the widget'sonTap/onPresseddirectly as a "fallback". Any hittable target fires twice — a counter goes +2, an expand/collapse toggle visibly does nothing.tap(text)only looked right because aTexthas no callback to re-fire.PointerDown/Move/UphadtimeStamp = 0. Flutter'sVelocityTrackerand scroll physics derive velocities from timestamps, soswipeon a PageView/TabBarView either snaps back (< half page) or is left mid-transition; on a real app the stuck drag then swallowed every later tap until a restart. Same indrag,swipe_coordinates, long press. The pointer was also never released if a handler threw.Future.delayed(300ms)after the input; on a heavier app the state query issued right after still saw the pre-tap tree.await endOfFrame; a minimized/occluded window renders no frames on Windows, so calls hung for minutes.dragcannot do long-press drags (LongPressDraggable, long-press-to-select lists): it moved immediately after pointer-down. The live schema (engine/tool_registry.dart) had no coordinate form;tool_definitions.dartis a stale second copy.visible≠ reachable.inspect/inspect_interactivereported cards under a bottom bar / FAB, or scrolled off-screen, asvisible: true; a drag started there landed on the bar.max_widthwhiletap_at/swipetake logical coordinates — an easy trap for agents (I fell into it).Color.withOpacitywith a negative alpha): 1000+ entries pollutingget_errors.Fix
SDK (
lib/flutter_skill.dart)_emitGesture(from, to, hold, travel), used by tap, long press, swipe, swipe_coordinates, drag and double tap: monotonically increasingtimeStamps from aStopwatch, ~one move per frame,PointerUpinfinally, then_settle()— two frames (the one that runs the callbacks and the one that paints the result), each bounded to 500 ms. When no frame arrives (window minimized/hidden) the action returns promptly and its response carrieswarning: "No frame was rendered after this action; the app window is probably minimized or hidden…".tap(key|text|ref):_isHittableAt(element, center)(RendererBinding.hitTestInView) decides — reachable → pointer only; covered/pointer-ignored → direct callback. Exactly one activation.inspect/inspect_interactivegainhittable(hit test at the element's center);visiblekeeps its old meaning ("has a size").dragandswipeCoordinatesaccepthold(ms).imageWidth/Height,logicalWidth/Height,devicePixelRatio.Server
dragtool:start_x/start_y/end_x/end_y,hold_ms,duration_msin the live registry (and the legacy copy); numeric args arriving as strings are accepted.screenshotaddsimage_size,logical_size,device_pixel_ratio,scaleand acoordinates_hint.tap/tap_at/swipe/dragresponses pass the SDKwarningthrough.Verification
Probe app (TabBar/TabBarView, ListView with a toggling group, counter button,
LongPressDraggable→DragTarget, rootListenerrecording pointer-down positions) driven over MCP stdio; indicators on and off.tap_ataccuracy at 4 screen points (incl. under the indicator bar)swipe→ next tab, settledDragTarget(keys, coordinates)Also on a real app (Strat·It Playbook, every move undone): template → group header, template → section header, strategy → folder, strategy → strategy (opens create-folder dialog), batch drag in selection mode, edge auto-scroll up/down. Actions got 3–4× faster (frame-settled instead of 300 ms sleeps). Minimized-window case verified: actions return in ~1 s with the warning instead of hanging.
dart analyzeclean on the touched files.Notes for reviewers
registerExtensionhandlers need a hot restart in the running app (hot reload swaps method bodies but not the registered closures).flutter_driver.dart. Happy to rebase ontomainonce fix(hot-reload): drive Flutter Tools' reloadSources/hotRestart VM services so hot_reload/hot_restart actually apply changes #58 lands.