Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 36 additions & 63 deletions ui/src/components/ToolDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useState } from "react";
import { FunctionCall } from "@/types";
import { ScrollArea } from "@radix-ui/react-scroll-area";
import { FunctionSquare, CheckCircle, Clock, Code, ChevronUp, ChevronDown, Loader2, Text, Check, Copy, AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { FunctionSquare, CheckCircle, Clock, Code, Loader2, Text, AlertCircle } from "lucide-react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { SmartContent, parseContentString } from "@/components/chat/SmartContent";
import { CollapsibleSection } from "@/components/chat/CollapsibleSection";

export type ToolCallStatus = "requested" | "executing" | "completed";

Expand All @@ -17,24 +17,15 @@ interface ToolDisplayProps {
isError?: boolean;
}


// ── Main component ─────────────────────────────────────────────────────────
const ToolDisplay = ({ call, result, status = "requested", isError = false }: ToolDisplayProps) => {
const [areArgumentsExpanded, setAreArgumentsExpanded] = useState(false);
const [areResultsExpanded, setAreResultsExpanded] = useState(false);
const [isCopied, setIsCopied] = useState(false);

const hasResult = result !== undefined;
const parsedResult = hasResult ? parseContentString(result.content) : null;

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(result?.content || "");
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
} catch (err) {
console.error("Failed to copy text:", err);
}
};

// Define UI elements based on status
const getStatusDisplay = () => {
if (isError && status === "executing") {
return (
Expand Down Expand Up @@ -80,8 +71,13 @@ const ToolDisplay = ({ call, result, status = "requested", isError = false }: To
}
};

const argsContent = <SmartContent data={call.args} />;
const resultContent = parsedResult !== null
? <SmartContent data={parsedResult} className={isError ? "text-red-600 dark:text-red-400" : ""} />
: null;

return (
<Card className={`w-full mx-auto my-1 min-w-full ${isError ? 'border-red-300' : ''}`}>
<Card className={`w-full mx-auto my-1 min-w-full ${isError ? "border-red-300" : ""}`}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-xs flex space-x-5">
<div className="flex items-center font-medium">
Expand All @@ -94,53 +90,30 @@ const ToolDisplay = ({ call, result, status = "requested", isError = false }: To
{getStatusDisplay()}
</div>
</CardHeader>
<CardContent>
<div className="space-y-2 mt-4">
<Button variant="ghost" size="sm" className="p-0 h-auto justify-start" onClick={() => setAreArgumentsExpanded(!areArgumentsExpanded)}>
<Code className="w-4 h-4 mr-2" />
<span className="mr-2">Arguments</span>
{areArgumentsExpanded ? <ChevronUp className="w-4 h-4 ml-auto" /> : <ChevronDown className="w-4 h-4 ml-auto" />}
</Button>
{areArgumentsExpanded && (
<div className="relative">
<ScrollArea className="max-h-96 overflow-y-auto p-4 w-full mt-2 bg-muted/50">
<pre className="text-sm whitespace-pre-wrap break-words">
{JSON.stringify(call.args, null, 2)}
</pre>
</ScrollArea>
</div>
)}
</div>
<div className="mt-4 w-full">
{status === "executing" && !hasResult && (
<div className="flex items-center gap-2 py-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm">Executing...</span>
</div>
)}
{hasResult && (
<>
<Button variant="ghost" size="sm" className="p-0 h-auto justify-start" onClick={() => setAreResultsExpanded(!areResultsExpanded)}>
<Text className="w-4 h-4 mr-2" />
<span className="mr-2">{isError ? "Error" : "Results"}</span>
{areResultsExpanded ? <ChevronUp className="w-4 h-4 ml-auto" /> : <ChevronDown className="w-4 h-4 ml-auto" />}
</Button>
{areResultsExpanded && (
<div className="relative">
<ScrollArea className={`max-h-96 overflow-y-auto p-4 w-full mt-2 ${isError ? 'bg-red-50 dark:bg-red-950/10' : ''}`}>
<pre className={`text-sm whitespace-pre-wrap break-words ${isError ? 'text-red-600 dark:text-red-400' : ''}`}>
{result.content}
</pre>
</ScrollArea>

<Button variant="ghost" size="sm" className="absolute top-2 right-2 p-2" onClick={handleCopy}>
{isCopied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
</Button>
</div>
)}
</>
)}
</div>
<CardContent className="space-y-1 pt-0">
<CollapsibleSection
icon={Code}
expanded={areArgumentsExpanded}
onToggle={() => setAreArgumentsExpanded(!areArgumentsExpanded)}
previewContent={argsContent}
expandedContent={argsContent}
/>
{status === "executing" && !hasResult && (
<div className="flex items-center gap-2 py-1">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm">Executing...</span>
</div>
)}
{hasResult && resultContent && (
<CollapsibleSection
icon={Text}
expanded={areResultsExpanded}
onToggle={() => setAreResultsExpanded(!areResultsExpanded)}
previewContent={resultContent}
expandedContent={resultContent}
errorStyle={isError}
/>
)}
</CardContent>
</Card>
);
Expand Down
93 changes: 52 additions & 41 deletions ui/src/components/chat/AgentCallDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useMemo, useState } from "react";
import Link from "next/link";
import { FunctionCall } from "@/types";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { convertToUserFriendlyName } from "@/lib/utils";
import { ChevronDown, ChevronUp, MessageSquare, Loader2, AlertCircle, CheckCircle } from "lucide-react";
import { MessageSquare, Loader2, AlertCircle, CheckCircle } from "lucide-react";
import KagentLogo from "../kagent-logo";
import { SmartContent, parseContentString } from "./SmartContent";
import { CollapsibleSection } from "./CollapsibleSection";

export type AgentCallStatus = "requested" | "executing" | "completed";

Expand All @@ -17,13 +20,22 @@ interface AgentCallDisplayProps {
isError?: boolean;
}

const AGENT_TOOL_NAME_RE = /^(.+)__NS__(.+)$/;



const AgentCallDisplay = ({ call, result, status = "requested", isError = false }: AgentCallDisplayProps) => {
const [areInputsExpanded, setAreInputsExpanded] = useState(false);
const [areResultsExpanded, setAreResultsExpanded] = useState(false);

const agentDisplay = useMemo(() => convertToUserFriendlyName(call.name), [call.name]);
const hasResult = result !== undefined;

const agentMatch = call.name.match(AGENT_TOOL_NAME_RE);
const functionCallLink = agentMatch
? `/agents/${agentMatch[1].replace(/_/g, "-")}/${agentMatch[2].replace(/_/g, "-")}/function-calls/${call.id}`
: null;

const getStatusDisplay = () => {
if (isError && status === "executing") {
return (
Expand Down Expand Up @@ -68,6 +80,12 @@ const AgentCallDisplay = ({ call, result, status = "requested", isError = false
}
};

const parsedResult = hasResult && result?.content ? parseContentString(result.content) : null;
const argsContent = <SmartContent data={call.args} />;
const resultContent = parsedResult !== null
? <SmartContent data={parsedResult} className={isError ? "text-red-600 dark:text-red-400" : ""} />
: null;

return (
<Card className={`w-full mx-auto my-1 min-w-full ${isError ? 'border-red-300' : ''}`}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
Expand All @@ -76,55 +94,48 @@ const AgentCallDisplay = ({ call, result, status = "requested", isError = false
<KagentLogo className="w-4 h-4 mr-2" />
{agentDisplay}
</div>
<div className="font-light">{call.id}</div>
<div className="font-light">
{functionCallLink ? (
<Link href={functionCallLink} className="text-blue-500 hover:underline">
{call.id}
</Link>
) : (
call.id
)}
</div>
</CardTitle>
<div className="flex justify-center items-center text-xs">
{getStatusDisplay()}
</div>
</CardHeader>
<CardContent>
<div className="space-y-2 mt-2">
<button className="text-xs flex items-center gap-2" onClick={() => setAreInputsExpanded(!areInputsExpanded)}>
<MessageSquare className="w-4 h-4" />
<span>Input</span>
{areInputsExpanded ? <ChevronUp className="w-4 h-4 ml-1" /> : <ChevronDown className="w-4 h-4 ml-1" />}
</button>
{areInputsExpanded && (
<div className="mt-2 bg-muted/50 p-3 rounded">
<pre className="text-sm whitespace-pre-wrap break-words">{JSON.stringify(call.args, null, 2)}</pre>
</div>
)}
</div>

<div className="mt-4 w-full">
{status === "executing" && !hasResult && (
<div className="flex items-center gap-2 py-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm">{agentDisplay} is responding...</span>
</div>
)}
{hasResult && result?.content && (
<div className="space-y-2">
<button className="text-xs flex items-center gap-2" onClick={() => setAreResultsExpanded(!areResultsExpanded)}>
<MessageSquare className="w-4 h-4" />
<span>Output</span>
{areResultsExpanded ? <ChevronUp className="w-4 h-4 ml-1" /> : <ChevronDown className="w-4 h-4 ml-1" />}
</button>
{areResultsExpanded && (
<div className={`mt-2 ${isError ? 'bg-red-50 dark:bg-red-950/10' : 'bg-muted/50'} p-3 rounded`}>
<pre className={`text-sm whitespace-pre-wrap break-words ${isError ? 'text-red-600 dark:text-red-400' : ''}`}>
{result?.content}
</pre>
</div>
)}
</div>
)}
</div>
<CardContent className="space-y-1 pt-0">
<CollapsibleSection
icon={MessageSquare}
expanded={areInputsExpanded}
onToggle={() => setAreInputsExpanded(!areInputsExpanded)}
previewContent={argsContent}
expandedContent={argsContent}
/>
{status === "executing" && !hasResult && (
<div className="flex items-center gap-2 py-1">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm">{agentDisplay} is responding...</span>
</div>
)}
{hasResult && resultContent && (
<CollapsibleSection
icon={MessageSquare}
expanded={areResultsExpanded}
onToggle={() => setAreResultsExpanded(!areResultsExpanded)}
previewContent={resultContent}
expandedContent={resultContent}
errorStyle={isError}
/>
)}
</CardContent>
</Card>
);
};

export default AgentCallDisplay;


43 changes: 31 additions & 12 deletions ui/src/components/chat/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Message, TextPart } from "@a2a-js/sdk";
import { TruncatableText } from "@/components/chat/TruncatableText";
import ToolCallDisplay from "@/components/chat/ToolCallDisplay";
import KagentLogo from "../kagent-logo";
import { ThumbsUp, ThumbsDown } from "lucide-react";
import { ThumbsUp, ThumbsDown, Copy, Check } from "lucide-react";
import { useState } from "react";
import { FeedbackDialog } from "./FeedbackDialog";
import { toast } from "sonner";
Expand All @@ -21,6 +21,7 @@ interface ChatMessageProps {
export default function ChatMessage({ message, allMessages, agentContext }: ChatMessageProps) {
const [feedbackDialogOpen, setFeedbackDialogOpen] = useState(false);
const [isPositiveFeedback, setIsPositiveFeedback] = useState(true);
const [copied, setCopied] = useState(false);

const textParts = message.parts?.filter(part => part.kind === "text") || [];
const content = textParts.map(part => (part as TextPart).text).join("");
Expand Down Expand Up @@ -108,6 +109,13 @@ export default function ChatMessage({ message, allMessages, agentContext }: Chat
}


const handleCopy = () => {
navigator.clipboard.writeText(String(content)).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
};
Comment on lines +112 to +117
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handleCopy function doesn't handle promise rejection. While there's a .then() call, there's no .catch() to handle clipboard write failures. This could lead to unhandled promise rejections in browsers where clipboard access is denied or unavailable. Other clipboard implementations in the codebase (e.g., SmartContent.tsx line 189-193 and CodeBlock.tsx line 31-37) use try-catch with async/await for proper error handling.

Copilot uses AI. Check for mistakes.

const handleFeedback = (isPositive: boolean) => {
if (!messageId) {
console.error("Message ID is undefined, cannot submit feedback.");
Expand All @@ -127,22 +135,33 @@ export default function ChatMessage({ message, allMessages, agentContext }: Chat
<div className="text-xs font-bold">{displayName}</div>
</div> : <div className="text-xs font-bold">{displayName}</div>}
<TruncatableText content={String(content)} className="break-all text-primary-foreground" />
{source !== "user" && messageId !== undefined && (
{source !== "user" && (
<div className="flex mt-2 justify-end items-center gap-2">
<button
onClick={() => handleFeedback(true)}
className="p-1 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
aria-label="Thumbs up"
>
<ThumbsUp className="w-4 h-4" />
</button>
<button
onClick={() => handleFeedback(false)}
onClick={handleCopy}
className="p-1 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
aria-label="Thumbs down"
aria-label="Copy to clipboard"
>
<ThumbsDown className="w-4 h-4" />
{copied ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
</button>
{messageId !== undefined && (
<>
<button
onClick={() => handleFeedback(true)}
className="p-1 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
aria-label="Thumbs up"
>
<ThumbsUp className="w-4 h-4" />
</button>
<button
onClick={() => handleFeedback(false)}
className="p-1 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
aria-label="Thumbs down"
>
<ThumbsDown className="w-4 h-4" />
</button>
</>
)}
</div>
)}
</div>
Expand Down
Loading
Loading