diff --git a/components/Chat/MarkdownMessage.tsx b/components/Chat/MarkdownMessage.tsx
index 9a86546..6f1a1c3 100644
--- a/components/Chat/MarkdownMessage.tsx
+++ b/components/Chat/MarkdownMessage.tsx
@@ -18,7 +18,7 @@ import {
Text,
Title,
} from '@mantine/core';
-import { ToolCallDisplay } from './ToolCallDisplay';
+import { ToolCall, ToolCallGroup } from './ToolCallDisplay';
import classes from './MarkdownMessage.module.css';
// Import KaTeX CSS for LaTeX rendering
import 'katex/dist/katex.min.css';
@@ -93,6 +93,47 @@ function parseContentWithToolCalls(content: string): ContentSegment[] {
return segments;
}
+/**
+ * Grouped segment type - either text or a group of consecutive tool calls
+ */
+interface GroupedSegment {
+ type: 'text' | 'toolGroup';
+ content?: string;
+ toolCalls?: ToolCall[];
+}
+
+/**
+ * Group consecutive tool call segments together
+ */
+function groupConsecutiveToolCalls(segments: ContentSegment[]): GroupedSegment[] {
+ const grouped: GroupedSegment[] = [];
+ let currentToolGroup: ToolCall[] = [];
+
+ for (const segment of segments) {
+ if (segment.type === 'tool' && segment.toolCall) {
+ // Add to current tool group
+ currentToolGroup.push(segment.toolCall);
+ } else {
+ // Flush any pending tool group before adding text
+ if (currentToolGroup.length > 0) {
+ grouped.push({ type: 'toolGroup', toolCalls: currentToolGroup });
+ currentToolGroup = [];
+ }
+ // Add text segment
+ if (segment.content) {
+ grouped.push({ type: 'text', content: segment.content });
+ }
+ }
+ }
+
+ // Flush any remaining tool group
+ if (currentToolGroup.length > 0) {
+ grouped.push({ type: 'toolGroup', toolCalls: currentToolGroup });
+ }
+
+ return grouped;
+}
+
/**
* Hook that tracks content changes and returns content with fade-in markers for new text
*/
@@ -334,21 +375,15 @@ export function MarkdownMessage({ content, isStreaming = false }: MarkdownMessag
);
}
- // When not streaming, parse and render tool calls
+ // When not streaming, parse and render tool calls (grouped)
const segments = parseContentWithToolCalls(content);
+ const groupedSegments = groupConsecutiveToolCalls(segments);
return (
- {segments.map((segment, index) => {
- if (segment.type === 'tool' && segment.toolCall) {
- return (
-
- );
+ {groupedSegments.map((segment, index) => {
+ if (segment.type === 'toolGroup' && segment.toolCalls) {
+ return
;
}
return
;
})}
diff --git a/components/Chat/ToolCallDisplay.module.css b/components/Chat/ToolCallDisplay.module.css
index 2100a25..6b3ea55 100644
--- a/components/Chat/ToolCallDisplay.module.css
+++ b/components/Chat/ToolCallDisplay.module.css
@@ -27,3 +27,45 @@
max-height: 200px;
overflow-y: auto;
}
+
+/* Group container for multiple tool calls */
+.groupContainer {
+ background-color: var(--mantine-color-default-hover);
+ overflow: hidden;
+}
+
+.groupHeader {
+ transition: background-color 0.15s ease;
+}
+
+.groupHeader:hover {
+ background-color: var(--mantine-color-default-hover);
+}
+
+.groupContent {
+ padding: var(--mantine-spacing-xs);
+ border-top: 1px solid var(--mantine-color-default-border);
+}
+
+/* Nested tool call inside group */
+.nestedContainer {
+ background-color: var(--mantine-color-body);
+ overflow: hidden;
+}
+
+.nestedContainer:not(:last-child) {
+ margin-bottom: var(--mantine-spacing-xs);
+}
+
+.nestedHeader {
+ transition: background-color 0.15s ease;
+}
+
+.nestedHeader:hover {
+ background-color: var(--mantine-color-default-hover);
+}
+
+.nestedContent {
+ padding: 0 var(--mantine-spacing-xs) var(--mantine-spacing-xs);
+ border-top: 1px solid var(--mantine-color-default-border);
+}
diff --git a/components/Chat/ToolCallDisplay.tsx b/components/Chat/ToolCallDisplay.tsx
index 43eede2..cffded2 100644
--- a/components/Chat/ToolCallDisplay.tsx
+++ b/components/Chat/ToolCallDisplay.tsx
@@ -6,10 +6,21 @@ import { ActionIcon, Code, Collapse, Group, Paper, Text, useMantineTheme } from
import { useThemeContext } from '@/components/DynamicThemeProvider';
import classes from './ToolCallDisplay.module.css';
+export interface ToolCall {
+ toolName: string;
+ args: Record
;
+ result: string;
+}
+
interface ToolCallDisplayProps {
toolName: string;
args: Record;
result: string;
+ nested?: boolean;
+}
+
+interface ToolCallGroupProps {
+ toolCalls: ToolCall[];
}
// Friendly tool names
@@ -25,12 +36,19 @@ const toolDisplayNames: Record = {
generate_image: 'Image Generation',
};
-export function ToolCallDisplay({ toolName, args, result }: ToolCallDisplayProps) {
+function getDisplayName(toolName: string): string {
+ return toolDisplayNames[toolName] || toolName;
+}
+
+/**
+ * Display a single tool call (can be standalone or nested inside a group)
+ */
+export function ToolCallDisplay({ toolName, args, result, nested = false }: ToolCallDisplayProps) {
const [opened, setOpened] = useState(false);
const { primaryColor } = useThemeContext();
const theme = useMantineTheme();
- const displayName = toolDisplayNames[toolName] || toolName;
+ const displayName = getDisplayName(toolName);
const isError = result.startsWith('Error:');
// Format args for display
@@ -38,10 +56,14 @@ export function ToolCallDisplay({ toolName, args, result }: ToolCallDisplayProps
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
.join(', ');
+ const containerClass = nested ? classes.nestedContainer : classes.container;
+ const headerClass = nested ? classes.nestedHeader : classes.header;
+ const contentClass = nested ? classes.nestedContent : classes.content;
+
return (
-
+
setOpened(!opened)}
gap="xs"
wrap="nowrap"
@@ -63,7 +85,7 @@ export function ToolCallDisplay({ toolName, args, result }: ToolCallDisplayProps
-
+
{Object.keys(args).length > 0 && (
@@ -87,3 +109,68 @@ export function ToolCallDisplay({ toolName, args, result }: ToolCallDisplayProps
);
}
+
+/**
+ * Display a group of consecutive tool calls in a single collapsible container
+ */
+export function ToolCallGroup({ toolCalls }: ToolCallGroupProps) {
+ const [opened, setOpened] = useState(false);
+ const { primaryColor } = useThemeContext();
+ const theme = useMantineTheme();
+
+ if (toolCalls.length === 0) {
+ return null;
+ }
+
+ // If only one tool call, render it directly without the group wrapper
+ if (toolCalls.length === 1) {
+ const tc = toolCalls[0];
+ return ;
+ }
+
+ // Get summary of tool names
+ const toolNames = toolCalls.map((tc) => getDisplayName(tc.toolName));
+ const uniqueTools = Array.from(new Set(toolNames));
+ const summary =
+ uniqueTools.length <= 2
+ ? uniqueTools.join(', ')
+ : `${uniqueTools.slice(0, 2).join(', ')} +${uniqueTools.length - 2} more`;
+
+ return (
+
+ setOpened(!opened)}
+ gap="xs"
+ wrap="nowrap"
+ p="xs"
+ style={{ cursor: 'pointer' }}
+ >
+
+ {opened ? : }
+
+
+
+ {toolCalls.length} Tool Calls
+
+
+ {summary}
+
+
+
+
+
+ {toolCalls.map((tc, index) => (
+
+ ))}
+
+
+
+ );
+}