-
Notifications
You must be signed in to change notification settings - Fork 523
Revert "Improve display of agent/tool cards in chat session" #1729
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"; | ||
| 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"; | ||
|
|
||
|
|
@@ -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) { | ||
|
|
@@ -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
|
||
| <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 && ( | ||
|
|
@@ -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> | ||
|
|
||
| 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 | ||||||||||||||||||
|
|
@@ -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") { | ||||||||||||||||||
|
|
@@ -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"> | ||||||||||||||||||
|
|
@@ -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"> | ||||||||||||||||||
|
||||||||||||||||||
| <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
AI
Apr 22, 2026
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ScrollAreais being imported directly from@radix-ui/react-scroll-area, but the codebase already wraps Radix in@/components/ui/scroll-area(addsViewport,ScrollBar, styling, and consistent API). Importing{ ScrollArea }from Radix is likely incorrect (Radix typically exportsRoot/Viewport/...rather than aScrollAreacomponent) and can break scrolling/rendering here. Switch this import to the local UI wrapper and keep usage the same.