Skip to content

Commit fbe6238

Browse files
author
GitLab CI
committed
fix: Resolve 7 MCP tool issues and add network monitoring
Bug fixes: - #1: connect_app/launch_app/scan_and_connect now auto-switch active session (eliminates session stacking causing "Service connection disposed" errors) - #3: Screenshot waits for frame render via endOfFrame before capture (fixes stale screenshots after navigation) - #6: assert_text falls back to reading child Text widgets (fixes null result for button text) - #8: screenshot_region now supports save_to_file (default: true) (prevents token overflow from large base64 output) - #9: get_errors supports limit/offset pagination (default: 50) (prevents token overflow from accumulated errors) - #10: inspect() filters to current page only by default (excludes off-screen elements from background pages) - #4: enter_text works without key - enters into focused TextField (tap a field first, then enter_text(text: "value")) New feature: - Network monitoring: enable_network_monitoring, get_network_requests, clear_network_requests tools for checking API responses after interactions Uses Dart VM Service HTTP profiling (captures all dart:io HTTP traffic)
1 parent 0c62eda commit fbe6238

3 files changed

Lines changed: 428 additions & 34 deletions

File tree

lib/flutter_skill.dart

Lines changed: 147 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class FlutterSkillBinding {
5959
static bool _registered = false;
6060
static final List<String> _logs = [];
6161
static final List<Map<String, dynamic>> _errors = [];
62+
static final List<Map<String, dynamic>> _httpRequests = [];
6263
static int _pointerCounter = 1;
6364

6465
// Test indicators
@@ -674,6 +675,64 @@ class FlutterSkillBinding {
674675
}
675676
});
676677

678+
// 25. Log HTTP Request (manual API for apps to call)
679+
developer.registerExtension('ext.flutter.flutter_skill.logHttpRequest',
680+
(method, parameters) async {
681+
try {
682+
final entry = <String, dynamic>{
683+
'method': parameters['method'] ?? 'GET',
684+
'url': parameters['url'] ?? '',
685+
'status_code': int.tryParse(parameters['status_code'] ?? ''),
686+
'duration_ms': int.tryParse(parameters['duration_ms'] ?? ''),
687+
'response_body': parameters['response_body'],
688+
'error': parameters['error'],
689+
'timestamp': DateTime.now().toIso8601String(),
690+
};
691+
// Remove null values
692+
entry.removeWhere((_, v) => v == null);
693+
_httpRequests.add(entry);
694+
if (_httpRequests.length > 500) {
695+
_httpRequests.removeAt(0);
696+
}
697+
return developer.ServiceExtensionResponse.result(
698+
jsonEncode({'logged': true, 'total': _httpRequests.length}));
699+
} catch (e, stack) {
700+
return _errorResponse(e, stack);
701+
}
702+
});
703+
704+
// 26. Get HTTP Requests (manually logged)
705+
developer.registerExtension('ext.flutter.flutter_skill.getHttpRequests',
706+
(method, parameters) async {
707+
try {
708+
final limit = int.tryParse(parameters['limit'] ?? '50') ?? 50;
709+
final offset = int.tryParse(parameters['offset'] ?? '0') ?? 0;
710+
final paged = _httpRequests.skip(offset).take(limit).toList();
711+
return developer.ServiceExtensionResponse.result(jsonEncode({
712+
'requests': paged,
713+
'total': _httpRequests.length,
714+
'returned': paged.length,
715+
'offset': offset,
716+
'limit': limit,
717+
}));
718+
} catch (e, stack) {
719+
return _errorResponse(e, stack);
720+
}
721+
});
722+
723+
// 27. Clear HTTP Requests
724+
developer.registerExtension('ext.flutter.flutter_skill.clearHttpRequests',
725+
(method, parameters) async {
726+
try {
727+
final count = _httpRequests.length;
728+
_httpRequests.clear();
729+
return developer.ServiceExtensionResponse.result(
730+
jsonEncode({'cleared': count}));
731+
} catch (e, stack) {
732+
return _errorResponse(e, stack);
733+
}
734+
});
735+
677736
// Setup error handler
678737
FlutterError.onError = (FlutterErrorDetails details) {
679738
_errors.add({
@@ -1027,20 +1086,91 @@ class FlutterSkillBinding {
10271086
return true;
10281087
}
10291088

1089+
/// Find the currently focused TextField's EditableTextState
1090+
static EditableTextState? _findFocusedTextField() {
1091+
EditableTextState? focused;
1092+
1093+
void visit(Element element) {
1094+
if (focused != null) return;
1095+
if (element is StatefulElement && element.state is EditableTextState) {
1096+
final state = element.state as EditableTextState;
1097+
if (state.widget.focusNode.hasFocus) {
1098+
focused = state;
1099+
return;
1100+
}
1101+
}
1102+
element.visitChildren(visit);
1103+
}
1104+
1105+
final binding = WidgetsBinding.instance;
1106+
// ignore: invalid_use_of_protected_member
1107+
if (binding.rootElement != null) {
1108+
visit(binding.rootElement!);
1109+
}
1110+
return focused;
1111+
}
1112+
10301113
/// Enhanced enter text with detailed error information
10311114
static Future<Map<String, dynamic>> _performEnterTextWithDetails(
10321115
{String? key, required String text}) async {
1116+
// If no key provided, try to enter text into the currently focused TextField
1117+
if (key == null) {
1118+
final focusedField = _findFocusedTextField();
1119+
if (focusedField != null) {
1120+
focusedField.updateEditingValue(TextEditingValue(
1121+
text: text,
1122+
selection: TextSelection.collapsed(offset: text.length),
1123+
));
1124+
_log('Entered text "$text" into focused TextField');
1125+
return {
1126+
'success': true,
1127+
'message': 'Text entered into focused TextField',
1128+
'method': 'focused_field',
1129+
'enteredText': text,
1130+
};
1131+
}
1132+
1133+
// No focused field found, try system channel as last resort
1134+
try {
1135+
await SystemChannels.textInput.invokeMethod('TextInput.setEditingState', {
1136+
'text': text,
1137+
'selectionBase': text.length,
1138+
'selectionExtent': text.length,
1139+
'composingBase': -1,
1140+
'composingExtent': -1,
1141+
});
1142+
_log('Text input sent via system channel (no key, no focus)');
1143+
return {
1144+
'success': true,
1145+
'message': 'Text entered via system channel (no focused TextField found)',
1146+
'method': 'system_channel',
1147+
'enteredText': text,
1148+
};
1149+
} catch (e) {
1150+
return {
1151+
'success': false,
1152+
'error': {
1153+
'code': ErrorCode.elementNotFound,
1154+
'message': 'No focused TextField found and system channel failed',
1155+
},
1156+
'suggestions': [
1157+
'Tap on a TextField first to focus it, then call enter_text(text: "...")',
1158+
'Or provide a key: enter_text(key: "field_key", text: "...")',
1159+
'Use inspect() to find TextField elements with keys',
1160+
],
1161+
};
1162+
}
1163+
}
1164+
10331165
final element = _findElement(key: key);
10341166

10351167
if (element == null) {
10361168
final suggestions = <String>[];
10371169

1038-
if (key != null) {
1039-
final similarKeys = _findSimilarKeys(key);
1040-
if (similarKeys.isNotEmpty) {
1041-
suggestions
1042-
.add('Similar keys found: ${similarKeys.take(5).toList()}');
1043-
}
1170+
final similarKeys = _findSimilarKeys(key);
1171+
if (similarKeys.isNotEmpty) {
1172+
suggestions
1173+
.add('Similar keys found: ${similarKeys.take(5).toList()}');
10441174
}
10451175

10461176
// Find TextField keys specifically
@@ -1056,6 +1186,7 @@ class FlutterSkillBinding {
10561186
}
10571187

10581188
suggestions.add('Use inspect() to find TextField elements');
1189+
suggestions.add('Or omit key to enter text into the currently focused TextField');
10591190

10601191
return {
10611192
'success': false,
@@ -1784,6 +1915,7 @@ class FlutterSkillBinding {
17841915
final element = _findElementByKey(key);
17851916
if (element == null) return null;
17861917

1918+
// First try: look for EditableTextState (TextField/TextFormField)
17871919
EditableTextState? editableTextState;
17881920
void findEditable(Element e) {
17891921
if (editableTextState != null) return;
@@ -1796,7 +1928,12 @@ class FlutterSkillBinding {
17961928

17971929
findEditable(element);
17981930

1799-
return editableTextState?.textEditingValue.text;
1931+
if (editableTextState != null) {
1932+
return editableTextState!.textEditingValue.text;
1933+
}
1934+
1935+
// Fallback: read child Text widget content (for buttons, labels, etc.)
1936+
return _extractTextFrom(element);
18001937
}
18011938

18021939
static bool? _getCheckboxState(String key) {
@@ -1881,6 +2018,9 @@ class FlutterSkillBinding {
18812018
return null;
18822019
}
18832020

2021+
// Wait for current frame to finish rendering (fixes stale screenshots after navigation)
2022+
await WidgetsBinding.instance.endOfFrame;
2023+
18842024
// Use quality as pixel ratio (lower = smaller image)
18852025
var pixelRatio = quality.clamp(0.1, 1.0);
18862026
var image = await boundary!.toImage(pixelRatio: pixelRatio);

0 commit comments

Comments
 (0)