Skip to content
Merged
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
23 changes: 0 additions & 23 deletions ui/src/components/ToolDisplay.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,6 @@ export const VeryLongUrl: Story = {
},
};

export const VeryLongToolId: Story = {
args: {
call: {
id: "call_super_duper_long_tool_identifier_that_goes_on_and_on_and_on_and_will_surely_overflow_if_not_truncated_properly_abc123_def456_ghi789",
name: "some_function",
args: {
path: "/src/components/App.tsx",
},
},
result: {
content: "Result text here",
},
status: "completed",
},
};

export const LongUnbreakableString: Story = {
args: {
call: {
Expand Down Expand Up @@ -150,13 +134,6 @@ export const InChatLayoutLongUrl: Story = {
},
};

export const InChatLayoutLongToolId: Story = {
decorators: [ChatLayoutDecorator],
args: {
...VeryLongToolId.args,
},
};

export const InChatLayoutUnbreakable: Story = {
decorators: [ChatLayoutDecorator],
args: {
Expand Down
105 changes: 64 additions & 41 deletions ui/src/components/ToolDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { useState } from "react";
import { FunctionCall, TokenStats } from "@/types";
import { FunctionSquare, CheckCircle, Clock, Code, Loader2, Text, AlertCircle, ShieldAlert } from "lucide-react";
import { ScrollArea } from "@radix-ui/react-scroll-area";
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

ScrollArea is being imported directly from @radix-ui/react-scroll-area, but the codebase already wraps Radix in @/components/ui/scroll-area (adds Viewport, ScrollBar, styling, and consistent API). Importing { ScrollArea } from Radix is likely incorrect (Radix typically exports Root/Viewport/... rather than a ScrollArea component) and can break scrolling/rendering here. Switch this import to the local UI wrapper and keep usage the same.

Suggested change
import { ScrollArea } from "@radix-ui/react-scroll-area";
import { ScrollArea } from "@/components/ui/scroll-area";

Copilot uses AI. Check for mistakes.
import { FunctionSquare, CheckCircle, Clock, Code, ChevronUp, ChevronDown, Loader2, Text, Check, Copy, AlertCircle, ShieldAlert } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import TokenStatsTooltip from "@/components/chat/TokenStatsTooltip";
import { convertToUserFriendlyName } from "@/lib/utils";
import { SmartContent, parseJsonOrString } from "@/components/chat/SmartContent";
import { CollapsibleSection } from "@/components/chat/CollapsibleSection";

export type ToolCallStatus = "requested" | "executing" | "completed" | "pending_approval" | "approved" | "rejected";

Expand All @@ -28,17 +27,25 @@ interface ToolDisplayProps {
tokenStats?: TokenStats;
}


// ── Main component ─────────────────────────────────────────────────────────
const ToolDisplay = ({ call, result, status = "requested", isError = false, isDecided = false, subagentName, onApprove, onReject, tokenStats }: ToolDisplayProps) => {
const [areArgumentsExpanded, setAreArgumentsExpanded] = useState(status === "pending_approval");
const [areResultsExpanded, setAreResultsExpanded] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [showRejectForm, setShowRejectForm] = useState(false);
const [rejectionReason, setRejectionReason] = useState("");

const hasResult = result !== undefined;
const parsedResult = hasResult ? parseJsonOrString(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);
}
};

const handleApprove = async () => {
if (!onApprove) {
Expand Down Expand Up @@ -136,49 +143,53 @@ const ToolDisplay = ({ call, result, status = "requested", isError = false, isDe
}
};

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

const borderClass = status === "pending_approval"
? 'border-amber-300 dark:border-amber-700'
: status === "rejected"
? 'border-red-300 dark:border-red-700'
: status === "approved"
? 'border-green-300 dark:border-green-700'
: isError
? 'border-red-300'
: '';
? 'border-amber-300 dark:border-amber-700'
: status === "rejected"
? 'border-red-300 dark:border-red-700'
: status === "approved"
? 'border-green-300 dark:border-green-700'
: isError
? 'border-red-300'
: '';

return (
<Card className={`w-full mx-auto my-1 min-w-full ${borderClass}`}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-xs flex space-x-5 min-w-0">
<div className="flex items-center font-medium shrink-0">
<CardTitle className="text-xs flex space-x-5">
<div className="flex items-center font-medium">
<FunctionSquare className="w-4 h-4 mr-2" />
{call.name}
</div>
{subagentName && (
<div className="flex items-center text-muted-foreground font-normal shrink-0">
<div className="flex items-center text-muted-foreground font-normal">
via {convertToUserFriendlyName(subagentName)} subagent
</div>
)}
<div className="font-light truncate min-w-0">{call.id}</div>
<div className="font-light">{call.id}</div>
</CardTitle>
Comment on lines 167 to 170
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

call.id is rendered without truncation/min-width constraints in the header flex row. Long tool IDs can overflow the card and break the chat layout. Consider restoring min-w-0/truncate on the id element (and/or min-w-0 on the flex container) so long IDs don’t force horizontal overflow.

Copilot uses AI. Check for mistakes.
<div className="flex items-center gap-2 text-xs shrink-0 pl-2">
<div className="flex items-center gap-2 text-xs">
{tokenStats && <TokenStatsTooltip stats={tokenStats} />}
{getStatusDisplay()}
</div>
</CardHeader>
<CardContent>
<CollapsibleSection
icon={Code}
expanded={areArgumentsExpanded}
onToggle={() => setAreArgumentsExpanded(!areArgumentsExpanded)}
previewContent={argsContent}
expandedContent={argsContent}
/>
<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>

{/* Approval buttons — hidden when decided (batch) or submitting */}
{status === "pending_approval" && !isSubmitting && !isDecided && !showRejectForm && (
Expand Down Expand Up @@ -242,20 +253,32 @@ const ToolDisplay = ({ call, result, status = "requested", isError = false, isDe

<div className="mt-4 w-full">
{status === "executing" && !hasResult && (
<div className="flex items-center gap-2 py-1">
<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 && resultContent && (
<CollapsibleSection
icon={Text}
expanded={areResultsExpanded}
onToggle={() => setAreResultsExpanded(!areResultsExpanded)}
previewContent={resultContent}
expandedContent={resultContent}
errorStyle={isError}
/>
{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>
Expand Down
80 changes: 41 additions & 39 deletions ui/src/components/chat/AgentCallDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { createContext, useContext, useMemo, useState, useEffect } from "react";
import { FunctionCall, TokenStats } from "@/types";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { convertToUserFriendlyName, isAgentToolName } from "@/lib/utils";
import { convertToUserFriendlyName } from "@/lib/utils";
import { ChevronDown, ChevronUp, MessageSquare, Loader2, AlertCircle, CheckCircle, Activity } from "lucide-react";
import KagentLogo from "../kagent-logo";
import TokenStatsTooltip from "@/components/chat/TokenStatsTooltip";
import { getSubagentSessionWithEvents } from "@/app/actions/sessions";
import { Message, Task } from "@a2a-js/sdk";
import { extractMessagesFromTasks } from "@/lib/messageHandlers";
import ChatMessage from "@/components/chat/ChatMessage";
import { SmartContent, parseJsonOrString } from "./SmartContent";
import { CollapsibleSection } from "./CollapsibleSection";

// Track and avoid too deep nested agent viewing to avoid UI issues
// In theory this works for infinite depth
Expand Down Expand Up @@ -134,9 +132,7 @@ const AgentCallDisplay = ({ call, result, status = "requested", isError = false,
const activityDepth = useContext(ActivityDepthContext);
const agentDisplay = useMemo(() => convertToUserFriendlyName(call.name), [call.name]);
const hasResult = result !== undefined;
const showActivitySection = !!subagentSessionId && !isError && activityDepth < MAX_ACTIVITY_DEPTH;

const isAgent = isAgentToolName(call.name);
const showActivitySection = !!subagentSessionId && !isError && activityDepth < MAX_ACTIVITY_DEPTH;

const getStatusDisplay = () => {
if (isError && status === "executing") {
Expand Down Expand Up @@ -182,12 +178,6 @@ const showActivitySection = !!subagentSessionId && !isError && activityDepth < M
}
};

const parsedResult = hasResult && result?.content ? parseJsonOrString(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 @@ -196,39 +186,51 @@ const showActivitySection = !!subagentSessionId && !isError && activityDepth < M
<KagentLogo className="w-4 h-4 mr-2" />
{agentDisplay}
</div>
<div className="font-light">
{call.id}
</div>
<div className="font-light">{call.id}</div>
</CardTitle>
<div className="flex items-center gap-2 text-xs">
{tokenStats && <TokenStatsTooltip stats={tokenStats} />}
{getStatusDisplay()}
</div>
</CardHeader>
<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>
<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">
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

The expanded Input section renders the full JSON.stringify(call.args) in an unconstrained <pre> with no max height or scroll container. For large tool args this can massively expand the message list and degrade usability/performance. Consider wrapping the <pre> in the existing ScrollArea UI component (or add max-h-* + overflow-y-auto) similar to ToolDisplay’s behavior.

Suggested change
<div className="mt-2 bg-muted/50 p-3 rounded">
<div className="mt-2 bg-muted/50 p-3 rounded max-h-96 overflow-y-auto">

Copilot uses AI. Check for mistakes.
<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>
Comment on lines +226 to +228
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

The expanded Output section renders result.content in an unconstrained <pre> (no max height/scroll container). Tool outputs can be very large (logs, file contents), which can blow up the chat layout. Consider adding a bounded scroll area (e.g., reuse the shared ScrollArea wrapper or add max-h-* + overflow-y-auto) while keeping the error styling.

Suggested change
<pre className={`text-sm whitespace-pre-wrap break-words ${isError ? 'text-red-600 dark:text-red-400' : ''}`}>
{result?.content}
</pre>
<div className="max-h-96 overflow-y-auto">
<pre className={`text-sm whitespace-pre-wrap break-words ${isError ? 'text-red-600 dark:text-red-400' : ''}`}>
{result?.content}
</pre>
</div>

Copilot uses AI. Check for mistakes.
</div>
)}
</div>
)}
</div>

{showActivitySection && (
<div className="mt-4 border-t pt-3">
Expand Down
20 changes: 1 addition & 19 deletions ui/src/components/chat/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TruncatableText } from "@/components/chat/TruncatableText";
import ToolCallDisplay from "@/components/chat/ToolCallDisplay";
import AskUserDisplay, { AskUserQuestion } from "@/components/chat/AskUserDisplay";
import KagentLogo from "../kagent-logo";
import { ThumbsUp, ThumbsDown, Copy, Check } from "lucide-react";
import { ThumbsUp, ThumbsDown } from "lucide-react";
import TokenStatsTooltip from "@/components/chat/TokenStatsTooltip";
import type { TokenStats } from "@/types";
import { useState } from "react";
Expand All @@ -29,7 +29,6 @@ interface ChatMessageProps {
export default function ChatMessage({ message, allMessages, agentContext, onApprove, onReject, onAskUserSubmit, pendingDecisions }: ChatMessageProps) {
const [feedbackDialogOpen, setFeedbackDialogOpen] = useState(false);
const [isPositiveFeedback, setIsPositiveFeedback] = useState(true);
const [copied, setCopied] = useState(false);

if (!message) return null;

Expand Down Expand Up @@ -151,16 +150,6 @@ export default function ChatMessage({ message, allMessages, agentContext, onAppr
}


const handleCopy = async () => {
try {
await navigator.clipboard.writeText(String(content));
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
/* clipboard unavailable */
}
};

const handleFeedback = (isPositive: boolean) => {
if (!messageId) {
console.error("Message ID is undefined, cannot submit feedback.");
Expand All @@ -183,13 +172,6 @@ export default function ChatMessage({ message, allMessages, agentContext, onAppr
{source !== "user" && (
<div className="flex mt-2 justify-end items-center gap-2">
{tokenStats && <TokenStatsTooltip stats={tokenStats} />}
<button
onClick={handleCopy}
className="p-1 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
aria-label="Copy to clipboard"
>
{copied ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
</button>
{messageId !== undefined && (
<>
<button
Expand Down
Loading
Loading