@@ -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