Add new features and update configurations for improved functionality
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import "package:gpt_markdown/gpt_markdown.dart";
|
||||
import "package:shadcn_flutter/shadcn_flutter.dart";
|
||||
|
||||
class AssistantBubble extends StatelessWidget {
|
||||
const AssistantBubble({super.key, required this.content});
|
||||
|
||||
final String content;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GptMarkdown(content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export "../../../../src/permissions/permission_types.dart" show PermissionDecision;
|
||||
@@ -0,0 +1,89 @@
|
||||
import "dart:convert";
|
||||
import "package:shadcn_flutter/shadcn_flutter.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.isPendingPermission = false,
|
||||
});
|
||||
|
||||
final String toolName;
|
||||
final Map<String, dynamic>? toolInput;
|
||||
final String? result;
|
||||
final bool isPendingPermission;
|
||||
|
||||
// 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 {
|
||||
final decoded = jsonDecode(rest);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
return (name, decoded);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return (name, null);
|
||||
}
|
||||
|
||||
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, isPendingPermission: isPendingPermission);
|
||||
case "Edit":
|
||||
return EditBubble(input: input, result: result, isPendingPermission: isPendingPermission);
|
||||
case "Read":
|
||||
return ReadBubble(input: input, result: result, isPendingPermission: isPendingPermission);
|
||||
case "Write":
|
||||
return WriteBubble(input: input, result: result, isPendingPermission: isPendingPermission);
|
||||
case "Glob":
|
||||
return GlobBubble(input: input, result: result, isPendingPermission: isPendingPermission);
|
||||
case "Grep":
|
||||
return GrepBubble(input: input, result: result, isPendingPermission: isPendingPermission);
|
||||
case "WebSearch":
|
||||
return WebSearchBubble(input: input, result: result, isPendingPermission: isPendingPermission);
|
||||
case "WebFetch":
|
||||
return WebFetchBubble(input: input, result: result, isPendingPermission: isPendingPermission);
|
||||
case "Advisor":
|
||||
return AdvisorBubble(input: input, result: result, isPendingPermission: isPendingPermission);
|
||||
|
||||
default:
|
||||
return DefaultToolBubble(toolName: toolName, input: toolInput, result: result, isPendingPermission: isPendingPermission);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import "package:shadcn_flutter/shadcn_flutter.dart";
|
||||
import "tool_bubble_base.dart";
|
||||
|
||||
class AdvisorBubble extends StatelessWidget {
|
||||
const AdvisorBubble({
|
||||
super.key,
|
||||
required this.input,
|
||||
this.result,
|
||||
this.isPendingPermission = false,
|
||||
});
|
||||
|
||||
final Map<String, dynamic> input;
|
||||
final String? result;
|
||||
final bool isPendingPermission;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final model = input["model"] as String? ?? "";
|
||||
|
||||
return ToolBubbleBase(
|
||||
toolName: "Advisor",
|
||||
icon: LucideIcons.brain,
|
||||
result: result,
|
||||
isPendingPermission: isPendingPermission,
|
||||
detail: model,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import "package:shadcn_flutter/shadcn_flutter.dart";
|
||||
import "tool_bubble_base.dart";
|
||||
|
||||
class BashBubble extends StatelessWidget {
|
||||
const BashBubble({
|
||||
super.key,
|
||||
required this.input,
|
||||
this.result,
|
||||
this.isPendingPermission = false,
|
||||
});
|
||||
|
||||
final Map<String, dynamic> input;
|
||||
final String? result;
|
||||
final bool isPendingPermission;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final command = input["command"] as String? ?? "";
|
||||
|
||||
return ToolBubbleBase(
|
||||
toolName: "Bash",
|
||||
icon: LucideIcons.terminal,
|
||||
result: result,
|
||||
isPendingPermission: isPendingPermission,
|
||||
detail: command,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import "dart:convert";
|
||||
import "package:shadcn_flutter/shadcn_flutter.dart";
|
||||
import "tool_bubble_base.dart";
|
||||
|
||||
class DefaultToolBubble extends StatelessWidget {
|
||||
const DefaultToolBubble({
|
||||
super.key,
|
||||
required this.toolName,
|
||||
this.input,
|
||||
this.result,
|
||||
this.isPendingPermission = false,
|
||||
});
|
||||
|
||||
final String toolName;
|
||||
final Map<String, dynamic>? input;
|
||||
final String? result;
|
||||
final bool isPendingPermission;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return ToolBubbleBase(
|
||||
toolName: toolName,
|
||||
icon: LucideIcons.wrench,
|
||||
result: result,
|
||||
isPendingPermission: isPendingPermission,
|
||||
body: input != null && input!.isNotEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Text(
|
||||
const JsonEncoder.withIndent(" ").convert(input),
|
||||
style: theme.typography.p.copyWith(
|
||||
fontSize: 12,
|
||||
color: theme.colorScheme.mutedForeground,
|
||||
fontFamily: "monospace",
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import "package:provider/provider.dart";
|
||||
import "package:shadcn_flutter/shadcn_flutter.dart";
|
||||
import "../../../../providers/chat_provider.dart";
|
||||
import "../../../../utils/path_utils.dart";
|
||||
import "../../diff_view.dart";
|
||||
import "tool_bubble_base.dart";
|
||||
|
||||
class EditBubble extends StatelessWidget {
|
||||
const EditBubble({
|
||||
super.key,
|
||||
required this.input,
|
||||
this.result,
|
||||
this.isPendingPermission = false,
|
||||
});
|
||||
|
||||
final Map<String, dynamic> input;
|
||||
final String? result;
|
||||
final bool isPendingPermission;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final projectRoot = context.read<ChatProvider>().workingDirectory;
|
||||
final filePath = input["file_path"] as String? ?? "";
|
||||
final oldString = input["old_string"] as String? ?? "";
|
||||
final newString = input["new_string"] as String? ?? "";
|
||||
|
||||
return ToolBubbleBase(
|
||||
toolName: "Edit",
|
||||
icon: LucideIcons.filePen,
|
||||
result: result,
|
||||
isPendingPermission: isPendingPermission,
|
||||
detail: shortenPath(filePath, projectRoot),
|
||||
body: DiffView(
|
||||
oldString: oldString,
|
||||
newString: newString,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import "package:provider/provider.dart";
|
||||
import "package:shadcn_flutter/shadcn_flutter.dart";
|
||||
import "../../../../providers/chat_provider.dart";
|
||||
import "../../../../utils/path_utils.dart";
|
||||
import "tool_bubble_base.dart";
|
||||
|
||||
class GlobBubble extends StatelessWidget {
|
||||
const GlobBubble({
|
||||
super.key,
|
||||
required this.input,
|
||||
this.result,
|
||||
this.isPendingPermission = false,
|
||||
});
|
||||
|
||||
final Map<String, dynamic> input;
|
||||
final String? result;
|
||||
final bool isPendingPermission;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final projectRoot = context.read<ChatProvider>().workingDirectory;
|
||||
final pattern = input["pattern"] as String? ?? "";
|
||||
final searchPath = input["path"] as String?;
|
||||
|
||||
final detail = searchPath != null && searchPath.isNotEmpty
|
||||
? "${shortenPath(searchPath, projectRoot)}/$pattern"
|
||||
: pattern;
|
||||
|
||||
return ToolBubbleBase(
|
||||
toolName: "Glob",
|
||||
icon: LucideIcons.folderSearch,
|
||||
result: result,
|
||||
isPendingPermission: isPendingPermission,
|
||||
detail: detail,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import "package:provider/provider.dart";
|
||||
import "package:shadcn_flutter/shadcn_flutter.dart";
|
||||
import "../../../../providers/chat_provider.dart";
|
||||
import "../../../../utils/path_utils.dart";
|
||||
import "tool_bubble_base.dart";
|
||||
|
||||
class GrepBubble extends StatelessWidget {
|
||||
const GrepBubble({
|
||||
super.key,
|
||||
required this.input,
|
||||
this.result,
|
||||
this.isPendingPermission = false,
|
||||
});
|
||||
|
||||
final Map<String, dynamic> input;
|
||||
final String? result;
|
||||
final bool isPendingPermission;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final projectRoot = context.read<ChatProvider>().workingDirectory;
|
||||
final pattern = input["pattern"] as String? ?? "";
|
||||
final searchPath = input["path"] as String?;
|
||||
|
||||
final detail = searchPath != null && searchPath.isNotEmpty
|
||||
? "${shortenPath(searchPath, projectRoot)} — $pattern"
|
||||
: pattern;
|
||||
|
||||
return ToolBubbleBase(
|
||||
toolName: "Grep",
|
||||
icon: LucideIcons.search,
|
||||
result: result,
|
||||
isPendingPermission: isPendingPermission,
|
||||
detail: detail,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import "package:provider/provider.dart";
|
||||
import "package:shadcn_flutter/shadcn_flutter.dart";
|
||||
import "../../../../providers/chat_provider.dart";
|
||||
import "../../../../utils/path_utils.dart";
|
||||
import "tool_bubble_base.dart";
|
||||
|
||||
class ReadBubble extends StatelessWidget {
|
||||
const ReadBubble({
|
||||
super.key,
|
||||
required this.input,
|
||||
this.result,
|
||||
this.isPendingPermission = false,
|
||||
});
|
||||
|
||||
final Map<String, dynamic> input;
|
||||
final String? result;
|
||||
final bool isPendingPermission;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final projectRoot = context.read<ChatProvider>().workingDirectory;
|
||||
final filePath = input["file_path"] as String? ?? "";
|
||||
|
||||
return ToolBubbleBase(
|
||||
toolName: "Read",
|
||||
icon: LucideIcons.fileText,
|
||||
result: result,
|
||||
isPendingPermission: isPendingPermission,
|
||||
detail: shortenPath(filePath, projectRoot),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import "package:provider/provider.dart";
|
||||
import "package:shadcn_flutter/shadcn_flutter.dart";
|
||||
import "../../../../providers/chat_provider.dart";
|
||||
import "../permission_decision.dart";
|
||||
|
||||
class ToolBubbleBase extends StatelessWidget {
|
||||
const ToolBubbleBase({
|
||||
super.key,
|
||||
required this.toolName,
|
||||
required this.icon,
|
||||
this.detail,
|
||||
this.body,
|
||||
this.result,
|
||||
this.isPendingPermission = false,
|
||||
});
|
||||
|
||||
final String toolName;
|
||||
final IconData icon;
|
||||
final String? detail;
|
||||
final Widget? body;
|
||||
final String? result;
|
||||
final bool isPendingPermission;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
|
||||
OutlinedContainer(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
IntrinsicHeight(
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
color: theme.colorScheme.primary.scaleAlpha(0.5),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon).iconSmall,
|
||||
|
||||
Gap(8),
|
||||
|
||||
Text(
|
||||
toolName,
|
||||
).textSmall,
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
VerticalDivider(),
|
||||
|
||||
if (detail != null)...[
|
||||
Gap(16),
|
||||
Text(
|
||||
detail!,
|
||||
).mono.xSmall
|
||||
]
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
|
||||
if (body != null) ...[
|
||||
Divider(),
|
||||
|
||||
body!,
|
||||
],
|
||||
|
||||
if (result != null) ...[
|
||||
Divider(),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: SelectableText(
|
||||
"\u200B${result!}",
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.mutedForeground,
|
||||
),
|
||||
).xSmall.mono,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (isPendingPermission) ...[
|
||||
|
||||
Gap(8),
|
||||
Row(
|
||||
children: [
|
||||
|
||||
Expanded(
|
||||
child: Button.outline(
|
||||
leading: Icon(LucideIcons.check).iconSmall,
|
||||
onPressed: () => context.read<ChatProvider>().resolvePermission(PermissionDecision.allowOnce),
|
||||
child: Text("Allow").small,
|
||||
),
|
||||
),
|
||||
|
||||
Gap(8),
|
||||
|
||||
Expanded(
|
||||
child: Button.outline(
|
||||
leading: Icon(LucideIcons.checkCheck).iconSmall,
|
||||
onPressed: () => context.read<ChatProvider>().resolvePermission(PermissionDecision.allowAlways),
|
||||
child: Text("Allow always").small,
|
||||
),
|
||||
),
|
||||
|
||||
Gap(8),
|
||||
|
||||
Expanded(
|
||||
child: Button.destructive(
|
||||
leading: Icon(LucideIcons.x).iconSmall,
|
||||
onPressed: () => context.read<ChatProvider>().resolvePermission(PermissionDecision.reject),
|
||||
child: Text("Reject").small,
|
||||
),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import "package:shadcn_flutter/shadcn_flutter.dart";
|
||||
import "tool_bubble_base.dart";
|
||||
|
||||
class WebFetchBubble extends StatelessWidget {
|
||||
const WebFetchBubble({
|
||||
super.key,
|
||||
required this.input,
|
||||
this.result,
|
||||
this.isPendingPermission = false,
|
||||
});
|
||||
|
||||
final Map<String, dynamic> input;
|
||||
final String? result;
|
||||
final bool isPendingPermission;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final url = input["url"] as String? ?? "";
|
||||
|
||||
return ToolBubbleBase(
|
||||
toolName: "WebFetch",
|
||||
icon: LucideIcons.link,
|
||||
result: result,
|
||||
isPendingPermission: isPendingPermission,
|
||||
detail: url,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import "package:shadcn_flutter/shadcn_flutter.dart";
|
||||
import "tool_bubble_base.dart";
|
||||
|
||||
class WebSearchBubble extends StatelessWidget {
|
||||
const WebSearchBubble({
|
||||
super.key,
|
||||
required this.input,
|
||||
this.result,
|
||||
this.isPendingPermission = false,
|
||||
});
|
||||
|
||||
final Map<String, dynamic> input;
|
||||
final String? result;
|
||||
final bool isPendingPermission;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final query = input["query"] as String? ?? "";
|
||||
|
||||
return ToolBubbleBase(
|
||||
toolName: "WebSearch",
|
||||
icon: LucideIcons.globe,
|
||||
result: result,
|
||||
isPendingPermission: isPendingPermission,
|
||||
detail: query,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import "package:provider/provider.dart";
|
||||
import "package:shadcn_flutter/shadcn_flutter.dart";
|
||||
import "../../../../providers/chat_provider.dart";
|
||||
import "../../../../utils/path_utils.dart";
|
||||
import "../../diff_view.dart";
|
||||
import "tool_bubble_base.dart";
|
||||
|
||||
class WriteBubble extends StatelessWidget {
|
||||
const WriteBubble({
|
||||
super.key,
|
||||
required this.input,
|
||||
this.result,
|
||||
this.isPendingPermission = false,
|
||||
});
|
||||
|
||||
final Map<String, dynamic> input;
|
||||
final String? result;
|
||||
final bool isPendingPermission;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final projectRoot = context.read<ChatProvider>().workingDirectory;
|
||||
final filePath = input["file_path"] as String? ?? "";
|
||||
final content = input["content"] as String? ?? "";
|
||||
|
||||
return ToolBubbleBase(
|
||||
toolName: "Write",
|
||||
icon: LucideIcons.filePlus,
|
||||
result: result,
|
||||
isPendingPermission: isPendingPermission,
|
||||
detail: shortenPath(filePath, projectRoot),
|
||||
body: DiffView(
|
||||
oldString: "",
|
||||
newString: content,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import "package:shadcn_flutter/shadcn_flutter.dart";
|
||||
|
||||
class UserBubble extends StatelessWidget {
|
||||
const UserBubble({super.key, required this.content});
|
||||
|
||||
final String content;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: OutlinedContainer(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
backgroundColor: Theme.of(context).colorScheme.border,
|
||||
child: SelectableText(content),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user