| 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'; |
| |
| /// Scrolls a widget or its scrollable ancestor/descendant. |
| class ScrollWidgetTool extends AgentTool<ScrollWidgetInput, ScrollWidgetOutput> { |
| const ScrollWidgetTool({ |
| required this.commandBus, |
| }); |
| |
| final AppCommandBus commandBus; |
| |
| @override |
| AgentToolId get toolId => AgentToolId.scrollWidget; |
| |
| @override |
| String get description => |
| 'Scroll a scrollable widget or scroll a specific widget into view in the running app preview using its node ID. ' |
| 'Supports scrolling by delta or ensuring visibility.'; |
| |
| @override |
| SchemanticType<ScrollWidgetInput> get inputSchema => ScrollWidgetInput.$schema; |
| |
| @override |
| SchemanticType<ScrollWidgetOutput> get outputSchema => ScrollWidgetOutput.$schema; |
| |
| Future<String?> _invokeExtension(String method, Map<String, String> args) { |
| return commandBus.dispatchAsync(InvokePreviewExtensionCommand( |
| method: method, |
| args: args, |
| )); |
| } |
| |
| @override |
| Future<ScrollWidgetOutput> run(ScrollWidgetInput input) async { |
| final state = await commandBus.dispatchAsync(RequestPreviewStateCommand()); |
| if (!state.hasActiveRuntime) { |
| return ScrollWidgetOutput( |
| success: false, |
| error: 'No app preview is running. Use controlApp to start the preview first.', |
| ); |
| } |
| |
| try { |
| final params = <String, String>{ |
| 'id': input.nodeId, |
| }; |
| if (input.ensureVisible != null) { |
| params['ensureVisible'] = '${input.ensureVisible}'; |
| } |
| if (input.delta != null) { |
| params['delta'] = '${input.delta}'; |
| } |
| |
| final responseStr = await _invokeExtension( |
| 'ext.vibepad.scroll', |
| params, |
| ); |
| |
| if (responseStr == null || responseStr.isEmpty) { |
| return ScrollWidgetOutput( |
| success: false, |
| error: 'Could not scroll. 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 ScrollWidgetOutput( |
| success: false, |
| error: decoded['error'] as String? ?? 'Failed to scroll.', |
| ); |
| } |
| |
| return ScrollWidgetOutput( |
| success: true, |
| ); |
| } catch (e) { |
| return ScrollWidgetOutput( |
| success: false, |
| error: 'Failed to scroll widget "${input.nodeId}": $e', |
| ); |
| } |
| } |
| |
| @override |
| AgentToolEvent mapStartToToolEvent(ScrollWidgetInput input) { |
| final action = input.ensureVisible == true |
| ? 'into view' |
| : (input.delta != null ? 'by delta ${input.delta}' : ''); |
| return createToolEvent( |
| status: ToolStatus.running, |
| summary: 'Scrolling widget "${input.nodeId}" $action'.trim(), |
| ); |
| } |
| |
| @override |
| AgentToolEvent mapToToolEvent( |
| ScrollWidgetInput input, |
| ScrollWidgetOutput output, |
| ) { |
| final action = input.ensureVisible == true |
| ? 'into view' |
| : (input.delta != null ? 'by delta ${input.delta}' : ''); |
| final summaryText = output.success |
| ? 'Scrolled widget "${input.nodeId}" $action' |
| : 'Failed to scroll widget "${input.nodeId}" $action'; |
| return createToolEvent( |
| status: output.success ? ToolStatus.success : ToolStatus.error, |
| summary: summaryText.trim(), |
| errorMessage: output.error, |
| ); |
| } |
| } |