blob: 041e0a725f4b97227df2e244033393fb22fce3ff [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';
/// 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,
);
}
}