feat(chat): show tool calls with expandable arguments in message history

Render tool-call-only assistant messages as compact cards with wrench
icon instead of empty bubbles. Click to expand and view tool arguments
as formatted JSON. Make thinking block full-width in chat thread.
This commit is contained in:
viettranx committed 2026-03-07 14:55:19 +07:00
1 parent 612d9fa965
commit 8efa34fff7
2 files changed
+66 -4

No files matched your search

+65 -1
View File
@@ -1,6 +1,8 @@
import { Bot, User } from "lucide-react";
import { useState } from "react";
import { Bot, User, Wrench, ChevronRight } from "lucide-react";
import { MessageContent } from "./message-content";
import type { ChatMessage } from "@/types/chat";
import type { ToolCall } from "@/types/session";
interface MessageBubbleProps {
message: ChatMessage;
@@ -14,6 +16,25 @@ export function MessageBubble({ message }: MessageBubbleProps) {
return null; // Tool messages are shown inline with assistant messages
}
const hasContent = !!message.content?.trim();
const hasToolCalls = message.toolCalls && message.toolCalls.length > 0;
// Skip assistant messages with neither text nor tool calls
if (!isUser && !hasContent && !hasToolCalls) {
return null;
}
// Tool-call-only assistant message: render compact tool call list
if (!isUser && !hasContent && hasToolCalls) {
return (
<div className="space-y-1">
{message.toolCalls!.map((tc) => (
<ToolCallItem key={tc.id} toolCall={tc} />
))}
</div>
);
}
return (
<div className={`flex gap-3 ${isUser ? "flex-row-reverse" : ""}`}>
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border bg-background">
@@ -31,6 +52,13 @@ export function MessageBubble({ message }: MessageBubbleProps) {
: "bg-card text-card-foreground border border-border shadow-sm"
}`}
>
{hasToolCalls && (
<div className="mb-2 space-y-1">
{message.toolCalls!.map((tc) => (
<ToolCallItem key={tc.id} toolCall={tc} compact />
))}
</div>
)}
<MessageContent content={message.content} role={message.role} />
{message.timestamp && (
<div className={`mt-1 text-[10px] ${isUser ? "text-primary-foreground/60" : "text-muted-foreground"}`}>
@@ -44,3 +72,39 @@ export function MessageBubble({ message }: MessageBubbleProps) {
</div>
);
}
function ToolCallItem({ toolCall, compact }: { toolCall: ToolCall; compact?: boolean }) {
const [expanded, setExpanded] = useState(false);
const hasArgs = toolCall.arguments && Object.keys(toolCall.arguments).length > 0;
return (
<div className={compact ? "" : "rounded-md border bg-muted/50 overflow-hidden"}>
<button
type="button"
onClick={() => hasArgs && setExpanded(!expanded)}
className={`flex items-center gap-1.5 w-full text-left ${
compact
? "text-xs text-muted-foreground py-0.5"
: "px-3 py-1.5 text-sm hover:bg-muted/80 transition-colors"
} ${hasArgs ? "cursor-pointer" : "cursor-default"}`}
>
{compact ? (
<Wrench className="h-3 w-3 shrink-0" />
) : (
<Wrench className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
)}
<span className={`font-mono truncate ${compact ? "" : "text-xs"}`}>{toolCall.name}</span>
{hasArgs && (
<ChevronRight className={`h-3 w-3 shrink-0 ml-auto text-muted-foreground transition-transform ${expanded ? "rotate-90" : ""}`} />
)}
</button>
{expanded && hasArgs && (
<pre className={`text-[11px] text-muted-foreground overflow-x-auto ${
compact ? "pl-4.5 pb-1" : "px-3 pb-2 border-t bg-muted/30"
}`}>
{JSON.stringify(toolCall.arguments, null, 2)}
</pre>
)}
</div>
);
}
+1 -3
View File
@@ -68,9 +68,7 @@ export function ChatThread({
{/* Thinking block (extended thinking / reasoning) */}
{isRunning && thinkingText && (
<div className="mx-auto max-w-[80%]">
<ThinkingBlock text={thinkingText} isStreaming={streamText === null} />
</div>
<ThinkingBlock text={thinkingText} isStreaming={streamText === null} />
)}
{/* Streaming text */}