blob: 5536bfbb65758db0648c7692ef4f0c81f9118657 [file]
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,
);
}
}