115 lines
4 KiB
Dart
115 lines
4 KiB
Dart
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<String, dynamic>? 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<String, dynamic>?) 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<String, dynamic>) {
|
|
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);
|
|
}
|
|
}
|
|
}
|