import "dart:convert"; import "package:shadcn_flutter/shadcn_flutter.dart"; import "../../../../src/permissions/permission_types.dart"; import "tools/advisor_bubble.dart"; import "tools/bash_bubble.dart"; import "tools/default_tool_bubble.dart"; import "tools/edit_bubble.dart"; import "tools/glob_bubble.dart"; import "tools/grep_bubble.dart"; import "tools/read_bubble.dart"; import "tools/web_fetch_bubble.dart"; import "tools/web_search_bubble.dart"; import "tools/write_bubble.dart"; class ToolBubble extends StatelessWidget { const ToolBubble({ super.key, required this.toolName, this.toolInput, this.result, this.pendingPermission, }); final String toolName; final Map? toolInput; final String? result; final PendingPermission? pendingPermission; // parse a tool message content string into (toolName, toolInput) // format: "$toolName call\n{json}" or "$toolName result\n..." static (String, Map?) parseContent(String content) { final newlineIdx = content.indexOf("\n"); if (newlineIdx == -1) { // no body, just a label line final name = _extractName(content); return (name, null); } final firstLine = content.substring(0, newlineIdx).trim(); final rest = content.substring(newlineIdx + 1).trim(); final name = _extractName(firstLine); if (firstLine.endsWith(" call") && rest.isNotEmpty) { try { // streamed output may be appended after the json block, so only // decode up to the closing brace — not the whole rest string final jsonEnd = _findJsonEnd(rest); final jsonStr = jsonEnd != -1 ? rest.substring(0, jsonEnd + 1) : rest; final decoded = jsonDecode(jsonStr); if (decoded is Map) { return (name, decoded); } } catch (_) {} } return (name, null); } // find the index of the closing } that ends the top-level json object static int _findJsonEnd(String s) { int depth = 0; bool inString = false; for (int i = 0; i < s.length; i++) { final c = s[i]; if (inString) { if (c == "\\" ) { i++; continue; } // skip escaped char if (c == "\"") inString = false; } else { if (c == "\"") inString = true; else if (c == "{") depth++; else if (c == "}") { depth--; if (depth == 0) return i; } } } return -1; } static String _extractName(String line) { // strip trailing " call" or " result" if (line.endsWith(" call")) return line.substring(0, line.length - 5).trim(); if (line.endsWith(" result")) return line.substring(0, line.length - 7).trim(); return line.trim(); } @override Widget build(BuildContext context) { final input = toolInput ?? {}; switch (toolName) { case "Bash": return BashBubble(input: input, result: result, pendingPermission: pendingPermission); case "Edit": return EditBubble(input: input, result: result, pendingPermission: pendingPermission); case "Read": return ReadBubble(input: input, result: result, pendingPermission: pendingPermission); case "Write": return WriteBubble(input: input, result: result, pendingPermission: pendingPermission); case "Glob": return GlobBubble(input: input, result: result, pendingPermission: pendingPermission); case "Grep": return GrepBubble(input: input, result: result, pendingPermission: pendingPermission); case "WebSearch": return WebSearchBubble(input: input, result: result, pendingPermission: pendingPermission); case "WebFetch": return WebFetchBubble(input: input, result: result, pendingPermission: pendingPermission); case "Advisor": return AdvisorBubble(input: input, result: result, pendingPermission: pendingPermission); default: return DefaultToolBubble(toolName: toolName, input: toolInput, result: result, pendingPermission: pendingPermission); } } }