| import 'dart:convert'; |
| |
| import 'package:schemantic/schemantic.dart'; |
| |
| import '../../../shared/logic/app_event_bus.dart'; |
| import '../../models/agent_tool_event.dart'; |
| import '../../models/agent_tool_id.dart'; |
| import '../base/agent_tool.dart'; |
| import '../tool_schemas.dart'; |
| |
| /// Finds widgets in the running preview matching query text or type. |
| class FindWidgetTool extends AgentTool<FindWidgetInput, FindWidgetOutput> { |
| const FindWidgetTool({ |
| required this.commandBus, |
| }); |
| |
| final AppCommandBus commandBus; |
| |
| @override |
| AgentToolId get toolId => AgentToolId.findWidget; |
| |
| @override |
| String get description => |
| 'Search for widgets in the running app preview by matching visible text or widget type. ' |
| 'Returns matching widgets with their node IDs to allow interaction (tap, enter text, scroll, details).'; |
| |
| @override |
| SchemanticType<FindWidgetInput> get inputSchema => FindWidgetInput.$schema; |
| |
| @override |
| SchemanticType<FindWidgetOutput> get outputSchema => FindWidgetOutput.$schema; |
| |
| Future<String?> _invokeExtension(String method, Map<String, String> args) { |
| return commandBus.dispatchAsync(InvokePreviewExtensionCommand( |
| method: method, |
| args: args, |
| )); |
| } |
| |
| @override |
| Future<FindWidgetOutput> run(FindWidgetInput input) async { |
| final state = await commandBus.dispatchAsync(RequestPreviewStateCommand()); |
| if (!state.hasActiveRuntime) { |
| return FindWidgetOutput( |
| success: false, |
| error: 'No app preview is running. Use controlApp to start the preview first.', |
| ); |
| } |
| |
| try { |
| final responseStr = await _invokeExtension( |
| 'ext.vibepad.findWidgets', |
| { |
| if (input.text != null) 'text': input.text!, |
| if (input.type != null) 'type': input.type!, |
| if (input.rootNodeId != null) 'rootNodeId': input.rootNodeId!, |
| }, |
| ); |
| |
| if (responseStr == null || responseStr.isEmpty) { |
| return FindWidgetOutput( |
| success: false, |
| error: 'Could not find widgets matching the query. No response from the running app.', |
| ); |
| } |
| |
| final Map<String, dynamic> decoded = jsonDecode(responseStr) as Map<String, dynamic>; |
| final success = decoded['success'] as bool? ?? false; |
| if (!success) { |
| return FindWidgetOutput( |
| success: false, |
| error: decoded['error'] as String? ?? 'Failed to search widgets.', |
| ); |
| } |
| |
| final rawMatches = decoded['matches'] as List<dynamic>? ?? const []; |
| final matches = rawMatches.map((m) { |
| final matchMap = m as Map<String, dynamic>; |
| return FindWidgetMatch( |
| id: matchMap['id'] as String, |
| widget: matchMap['widget'] as String, |
| ); |
| }).toList(); |
| |
| return FindWidgetOutput( |
| success: true, |
| matches: matches, |
| ); |
| } catch (e) { |
| return FindWidgetOutput( |
| success: false, |
| error: 'Failed to search widgets: $e', |
| ); |
| } |
| } |
| |
| String _truncate(String val, int maxLength) { |
| if (val.length <= maxLength) { |
| return val; |
| } |
| return '${val.substring(0, maxLength)}...'; |
| } |
| |
| String _formatQuery(FindWidgetInput input) { |
| final queries = [ |
| if (input.type != null) 'type: ${input.type}', |
| if (input.text != null) 'text: "${_truncate(input.text!, 20)}"', |
| ].join(', '); |
| return queries.isEmpty ? 'any' : queries; |
| } |
| |
| @override |
| AgentToolEvent mapStartToToolEvent(FindWidgetInput input) { |
| return createToolEvent( |
| status: ToolStatus.running, |
| summary: 'Finding widgets (${_formatQuery(input)})', |
| ); |
| } |
| |
| @override |
| AgentToolEvent mapToToolEvent( |
| FindWidgetInput input, |
| FindWidgetOutput output, |
| ) { |
| final count = output.matches?.length ?? 0; |
| final matchWord = count == 1 ? 'widget' : 'widgets'; |
| final queryStr = _formatQuery(input); |
| return createToolEvent( |
| status: output.success ? ToolStatus.success : ToolStatus.error, |
| summary: output.success ? 'Found $count $matchWord ($queryStr)' : 'Failed to find widgets ($queryStr)', |
| errorMessage: output.error, |
| ); |
| } |
| } |