Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mcp_server_dart/lib/src/shared_core/command_executor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1088,6 +1088,7 @@ final class DefaultCoreCommandExecutor implements CoreCommandExecutor {
'fromRef': command.fromRef,
'toRef': command.toRef,
if (command.snapshotId != null) 'snapshotId': command.snapshotId,
if (command.kind case final kind?) 'kind': kind.wireName,
},
);
return CoreResult.success(data: _map(result.json));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,7 @@ final class CommandCatalog {
fromRef: _stringArg(args, 'fromRef', fallback: ''),
toRef: _stringArg(args, 'toRef', fallback: ''),
snapshotId: _nullableIntArg(args, 'snapshotId', alias: 'snapshot-id'),
kind: parseDragPointerKind(_nullableStringArg(args, 'kind')),
),
),
CommandSpec(
Expand Down
47 changes: 43 additions & 4 deletions mcp_toolkit/lib/src/services/gesture_interaction_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/foundation.dart'
show TargetPlatform, defaultTargetPlatform, kIsWeb;
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
Expand Down Expand Up @@ -705,9 +706,16 @@ mixin GestureInteractionService {
}

/// Drag from the centre of [fromRef] to the centre of [toRef].
///
/// [kind] selects the synthesized pointer device; when null, defaults to
/// [PointerDeviceKind.mouse] on desktop platforms and
/// [PointerDeviceKind.touch] elsewhere — the device a real user would
/// drag with there. See [_dispatchDrag] for how the kind decides the
/// gesture-arena outcome.
static Future<Map<String, Object?>> drag({
required final String fromRef,
required final String toRef,
final PointerDeviceKind? kind,
}) async {
final from = SemanticSnapshotService.resolveCenter(fromRef);
if (from == null) {
Expand Down Expand Up @@ -735,18 +743,29 @@ mixin GestureInteractionService {
'mutate state directly via evaluate_dart_expression.',
};
}
await _dispatchDrag(from, to, steps: 12);
final effectiveKind = kind ?? _defaultDragKind();
await _dispatchDrag(from, to, steps: 12, kind: effectiveKind);
return <String, Object?>{
'success': true,
'via': 'pointer_events',
'action': 'drag',
'kind': effectiveKind.name,
'fromRef': fromRef,
'toRef': toRef,
'from': _offsetToMap(from),
'to': _offsetToMap(to),
};
}

/// The pointer device a real user drags with on the running platform.
static PointerDeviceKind _defaultDragKind() =>
switch (defaultTargetPlatform) {
TargetPlatform.macOS ||
TargetPlatform.windows ||
TargetPlatform.linux => PointerDeviceKind.mouse,
_ => PointerDeviceKind.touch,
};

/// Synthesize a mouse hover at the centre of the widget identified by
/// [ref]. Drives `MouseRegion.onEnter`/`onExit` via the framework's
/// mouse tracker (which computes enter/exit transitions from position
Expand Down Expand Up @@ -853,16 +872,30 @@ mixin GestureInteractionService {
await _waitFrame();
}

/// Dispatches a press-move-release sequence as [kind] pointer events.
///
/// [kind] decides which recognizers compete for the gesture. Mouse wins a
/// drag-and-drop cleanly on desktop: scrollables don't track mouse drags
/// (default [ScrollBehavior.dragDevices] excludes the mouse), so the
/// target's pan recognizer takes the gesture uncontested — matching a
/// real user drag. Touch keeps scrollables in the arena, which a
/// swipe/fling relies on.
static Future<void> _dispatchDrag(
final ui.Offset from,
final ui.Offset to, {
final int steps = 10,
final Duration perStep = const Duration(milliseconds: 16),
final PointerDeviceKind kind = PointerDeviceKind.touch,
}) async {
final binding = GestureBinding.instance;
final pointer = _nextPointerId++;
binding.handlePointerEvent(
PointerDownEvent(pointer: pointer, position: from, timeStamp: _now()),
PointerDownEvent(
pointer: pointer,
position: from,
kind: kind,
timeStamp: _now(),
),
);

final dx = (to.dx - from.dx) / steps;
Expand All @@ -876,14 +909,20 @@ mixin GestureInteractionService {
pointer: pointer,
position: pos,
delta: pos - last,
kind: kind,
timeStamp: _now(),
),
);
last = pos;
}

binding.handlePointerEvent(
PointerUpEvent(pointer: pointer, position: to, timeStamp: _now()),
PointerUpEvent(
pointer: pointer,
position: to,
kind: kind,
timeStamp: _now(),
),
);
await _waitFrame();
}
Expand Down
7 changes: 7 additions & 0 deletions mcp_toolkit/lib/src/toolkits/interaction_toolkit.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:dart_mcp/client.dart';
import 'package:flutter/gestures.dart' show PointerDeviceKind;
import 'package:flutter_mcp_toolkit_core/flutter_mcp_toolkit_core.dart';
import 'package:from_json_to_json/from_json_to_json.dart';
import 'package:intentcall_core/intentcall_core.dart';
Expand Down Expand Up @@ -487,9 +488,15 @@ extension type OnDragEntry._(AgentCallEntry entry) implements AgentCallEntry {
},
);
}
final kind = switch (parameters['kind']) {
'mouse' => PointerDeviceKind.mouse,
'touch' => PointerDeviceKind.touch,
_ => null,
};
final result = await GestureInteractionService.drag(
fromRef: fromRef,
toRef: toRef,
kind: kind,
);
return MCPCallResult(
message: result['success'] == true
Expand Down
40 changes: 40 additions & 0 deletions packages/core/lib/src/commands/core_commands.dart
Original file line number Diff line number Diff line change
Expand Up @@ -389,17 +389,57 @@ final class SwipeCommand extends CoreCommand {
String get name => 'swipe';
}

/// Pointer device to synthesize for a [DragCommand].
///
/// Keeps command state typed; the wire protocol stays stringly. Values are
/// parsed once via [parseDragPointerKind] and serialized back through
/// [wireName] only at the extension-forwarding boundary.
enum DragPointerKind {
/// Desktop-style pointer: scrollables ignore mouse drags, so the drag
/// reaches the target's drag/pan recognizer (drag-and-drop).
mouse('mouse'),

/// Touch-style pointer: scrollables compete for the gesture, so a drag
/// over scrollable content scrolls it.
touch('touch');

const DragPointerKind(this.wireName);

/// String form used on the wire (tool schema enum and extension args).
final String wireName;
}

/// Parses a wire [value] into a [DragPointerKind].
///
/// Returns null for null, empty and unknown values — the app then picks
/// the platform default.
DragPointerKind? parseDragPointerKind(final Object? value) {
if (value == null) return null;
final normalized = '$value'.trim().toLowerCase();
for (final kind in DragPointerKind.values) {
if (kind.wireName == normalized) {
return kind;
}
}
return null;
}
Comment on lines +392 to +425

@coderabbitai coderabbitai Bot Aug 2, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Complete the dartdoc for the new public contract.

DragPointerKind and parseDragPointerKind are public. The comments do not
include the required @ai guidance. They also do not document the public
mouse, touch, or wireName members. The parser comment does not document
the value parameter or the nullable return value.

Add concise, why-focused /// documentation. Explain that the enum keeps
command state typed and that wireName is used only at the forwarding
boundary. Document the parser input and null fallback.

Proposed documentation
-/// Pointer device to synthesize for a [DragCommand].
+/// Selects the pointer device used by [DragCommand].
+///
+/// This keeps command state typed while preserving explicit wire values at
+/// the forwarding boundary.
+///
+/// `@ai` Use this enum for command state. Use [wireName] only when forwarding.
 enum DragPointerKind {
+  /// Synthesizes mouse pointer events.
   mouse('mouse'),
+  /// Synthesizes touch pointer events.
   touch('touch');
 
+  /// Creates a pointer kind with its wire value.
   const DragPointerKind(this.wireName);
 
+  /// The value used when this kind is serialized for forwarding.
   final String wireName;
 }
 
-/// Parses a wire value into a [DragPointerKind]. Null, empty and unknown
-/// values yield null — the app picks the platform default.
+/// Parses [value] into a [DragPointerKind].
+///
+/// [value] is the raw command-boundary value.
+/// Returns the matching kind, or `null` for null, empty, and unknown values.
+/// A `null` result lets the app select the platform default.

As per coding guidelines, **/*.dart requires why-focused ///
documentation, an @ai annotation for public types, and documentation for
public members and parameters.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Pointer device to synthesize for a [DragCommand].
enum DragPointerKind {
mouse('mouse'),
touch('touch');
const DragPointerKind(this.wireName);
final String wireName;
}
/// Parses a wire value into a [DragPointerKind]. Null, empty and unknown
/// values yield null — the app picks the platform default.
DragPointerKind? parseDragPointerKind(final Object? value) {
if (value == null) return null;
final normalized = '$value'.trim().toLowerCase();
for (final kind in DragPointerKind.values) {
if (kind.wireName == normalized) {
return kind;
}
}
return null;
}
/// Selects the pointer device used by [DragCommand].
///
/// This keeps command state typed while preserving explicit wire values at
/// the forwarding boundary.
///
/// `@ai` Use this enum for command state. Use [wireName] only when forwarding.
enum DragPointerKind {
/// Synthesizes mouse pointer events.
mouse('mouse'),
/// Synthesizes touch pointer events.
touch('touch');
/// Creates a pointer kind with its wire value.
const DragPointerKind(this.wireName);
/// The value used when this kind is serialized for forwarding.
final String wireName;
}
/// Parses [value] into a [DragPointerKind].
///
/// [value] is the raw command-boundary value.
/// Returns the matching kind, or `null` for null, empty, and unknown values.
/// A `null` result lets the app select the platform default.
DragPointerKind? parseDragPointerKind(final Object? value) {
if (value == null) return null;
final normalized = '$value'.trim().toLowerCase();
for (final kind in DragPointerKind.values) {
if (kind.wireName == normalized) {
return kind;
}
}
return null;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/lib/src/commands/core_commands.dart` around lines 392 - 413,
Complete the dartdoc for public DragPointerKind, its mouse and touch members,
and wireName with concise why-focused descriptions and the required `@ai`
annotations; explain that the enum keeps command state typed and wireName is
used only at the forwarding boundary. Update parseDragPointerKind documentation
to describe the value input, normalization/unknown handling, and nullable null
fallback.

Source: Coding guidelines

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs completed in a834cdb: enum purpose (typed command state, wireName only at the forwarding boundary), both values, wireName, and the parser's value input + null fallback.

Skipped the @ai annotation deliberately: there are zero @ai occurrences anywhere in this repository and I couldn't find a guideline file requiring it — neighboring public types (ScreenshotMode, parseScreenshotMode) carry no such marker either. If maintainers can point to the convention, happy to add it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dipsy, thank you. The completed documentation addresses the substantive part of this finding.

The @ai requirement came from the configured Dart review guidelines, not from an existing repository convention. Your repository search is valid evidence that this requirement has no current precedent.

Please add the concise @ai guidance if the configured guideline is intended to apply to this repository. Otherwise, maintainers should remove or scope that guideline to prevent similar findings.

You are interacting with an AI system.


final class DragCommand extends CoreCommand {
const DragCommand({
required this.fromRef,
required this.toRef,
this.snapshotId,
this.kind,
});

final String fromRef;
final String toRef;
final int? snapshotId;

/// Pointer device kind to synthesize. Null lets the app pick the
/// platform default.
final DragPointerKind? kind;

@override
String get name => 'drag';
}
Expand Down
10 changes: 10 additions & 0 deletions packages/core/lib/src/tools/interaction_input_schemas.dart
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,16 @@ Map<String, Object?> dragInputSchema() => <String, Object?>{
'type': 'string',
'description': 'Target widget ref.',
},
'kind': <String, Object?>{
'type': 'string',
'enum': <String>['mouse', 'touch'],
'description':
'Pointer device to synthesize. Defaults to mouse on desktop '
'targets and touch elsewhere. Mouse keeps scrollables out of the '
'gesture arena, so the drag lands on the target\'s drag/pan '
'recognizer (drag-and-drop); touch keeps scrollables competing, '
'so a drag over scrollable content scrolls it instead.',
},
'snapshotId': <String, Object?>{
'type': 'integer',
'description':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,16 @@ void registerInteractionTools(final CapabilityContext context) {
final fromRef = stringArgOrNull(args['fromRef']) ?? '';
final toRef = stringArgOrNull(args['toRef']) ?? '';
final snapshotId = intArgOrNull(args['snapshotId']);
final kind = parseDragPointerKind(args['kind']);
return runCommand(
runner,
args,
DragCommand(fromRef: fromRef, toRef: toRef, snapshotId: snapshotId),
DragCommand(
fromRef: fromRef,
toRef: toRef,
snapshotId: snapshotId,
kind: kind,
),
);
},
),
Expand Down
Loading