11import { useState } from "react" ;
22import { FunctionCall , TokenStats } from "@/types" ;
3- import { FunctionSquare , CheckCircle , Clock , Code , Loader2 , Text , AlertCircle , ShieldAlert } from "lucide-react" ;
3+ import { ScrollArea } from "@radix-ui/react-scroll-area" ;
4+ import { FunctionSquare , CheckCircle , Clock , Code , ChevronUp , ChevronDown , Loader2 , Text , Check , Copy , AlertCircle , ShieldAlert } from "lucide-react" ;
45import { Button } from "@/components/ui/button" ;
56import { Textarea } from "@/components/ui/textarea" ;
67import { Card , CardHeader , CardTitle , CardContent } from "@/components/ui/card" ;
78import TokenStatsTooltip from "@/components/chat/TokenStatsTooltip" ;
89import { convertToUserFriendlyName } from "@/lib/utils" ;
9- import { SmartContent , parseJsonOrString } from "@/components/chat/SmartContent" ;
10- import { CollapsibleSection } from "@/components/chat/CollapsibleSection" ;
1110
1211export type ToolCallStatus = "requested" | "executing" | "completed" | "pending_approval" | "approved" | "rejected" ;
1312
@@ -28,17 +27,25 @@ interface ToolDisplayProps {
2827 tokenStats ?: TokenStats ;
2928}
3029
31-
32- // ── Main component ─────────────────────────────────────────────────────────
3330const ToolDisplay = ( { call, result, status = "requested" , isError = false , isDecided = false , subagentName, onApprove, onReject, tokenStats } : ToolDisplayProps ) => {
3431 const [ areArgumentsExpanded , setAreArgumentsExpanded ] = useState ( status === "pending_approval" ) ;
3532 const [ areResultsExpanded , setAreResultsExpanded ] = useState ( false ) ;
33+ const [ isCopied , setIsCopied ] = useState ( false ) ;
3634 const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
3735 const [ showRejectForm , setShowRejectForm ] = useState ( false ) ;
3836 const [ rejectionReason , setRejectionReason ] = useState ( "" ) ;
3937
4038 const hasResult = result !== undefined ;
41- const parsedResult = hasResult ? parseJsonOrString ( result . content ) : null ;
39+
40+ const handleCopy = async ( ) => {
41+ try {
42+ await navigator . clipboard . writeText ( result ?. content || "" ) ;
43+ setIsCopied ( true ) ;
44+ setTimeout ( ( ) => setIsCopied ( false ) , 2000 ) ;
45+ } catch ( err ) {
46+ console . error ( "Failed to copy text:" , err ) ;
47+ }
48+ } ;
4249
4350 const handleApprove = async ( ) => {
4451 if ( ! onApprove ) {
@@ -136,49 +143,53 @@ const ToolDisplay = ({ call, result, status = "requested", isError = false, isDe
136143 }
137144 } ;
138145
139- const argsContent = < SmartContent data = { call . args } /> ;
140- const resultContent = parsedResult !== null
141- ? < SmartContent data = { parsedResult } className = { isError ? "text-red-600 dark:text-red-400" : "" } />
142- : null ;
143-
144146 const borderClass = status === "pending_approval"
145- ? 'border-amber-300 dark:border-amber-700'
146- : status === "rejected"
147- ? 'border-red-300 dark:border-red-700'
148- : status === "approved"
149- ? 'border-green-300 dark:border-green-700'
150- : isError
151- ? 'border-red-300'
152- : '' ;
147+ ? 'border-amber-300 dark:border-amber-700'
148+ : status === "rejected"
149+ ? 'border-red-300 dark:border-red-700'
150+ : status === "approved"
151+ ? 'border-green-300 dark:border-green-700'
152+ : isError
153+ ? 'border-red-300'
154+ : '' ;
153155
154156 return (
155157 < Card className = { `w-full mx-auto my-1 min-w-full ${ borderClass } ` } >
156158 < CardHeader className = "flex flex-row items-center justify-between space-y-0 pb-2" >
157- < CardTitle className = "text-xs flex space-x-5 min-w-0 " >
158- < div className = "flex items-center font-medium shrink-0 " >
159+ < CardTitle className = "text-xs flex space-x-5" >
160+ < div className = "flex items-center font-medium" >
159161 < FunctionSquare className = "w-4 h-4 mr-2" />
160162 { call . name }
161163 </ div >
162164 { subagentName && (
163- < div className = "flex items-center text-muted-foreground font-normal shrink-0 " >
165+ < div className = "flex items-center text-muted-foreground font-normal" >
164166 via { convertToUserFriendlyName ( subagentName ) } subagent
165167 </ div >
166168 ) }
167- < div className = "font-light truncate min-w-0 " > { call . id } </ div >
169+ < div className = "font-light" > { call . id } </ div >
168170 </ CardTitle >
169- < div className = "flex items-center gap-2 text-xs shrink-0 pl-2 " >
171+ < div className = "flex items-center gap-2 text-xs" >
170172 { tokenStats && < TokenStatsTooltip stats = { tokenStats } /> }
171173 { getStatusDisplay ( ) }
172174 </ div >
173175 </ CardHeader >
174176 < CardContent >
175- < CollapsibleSection
176- icon = { Code }
177- expanded = { areArgumentsExpanded }
178- onToggle = { ( ) => setAreArgumentsExpanded ( ! areArgumentsExpanded ) }
179- previewContent = { argsContent }
180- expandedContent = { argsContent }
181- />
177+ < div className = "space-y-2 mt-4" >
178+ < Button variant = "ghost" size = "sm" className = "p-0 h-auto justify-start" onClick = { ( ) => setAreArgumentsExpanded ( ! areArgumentsExpanded ) } >
179+ < Code className = "w-4 h-4 mr-2" />
180+ < span className = "mr-2" > Arguments</ span >
181+ { areArgumentsExpanded ? < ChevronUp className = "w-4 h-4 ml-auto" /> : < ChevronDown className = "w-4 h-4 ml-auto" /> }
182+ </ Button >
183+ { areArgumentsExpanded && (
184+ < div className = "relative" >
185+ < ScrollArea className = "max-h-96 overflow-y-auto p-4 w-full mt-2 bg-muted/50" >
186+ < pre className = "text-sm whitespace-pre-wrap break-words" >
187+ { JSON . stringify ( call . args , null , 2 ) }
188+ </ pre >
189+ </ ScrollArea >
190+ </ div >
191+ ) }
192+ </ div >
182193
183194 { /* Approval buttons — hidden when decided (batch) or submitting */ }
184195 { status === "pending_approval" && ! isSubmitting && ! isDecided && ! showRejectForm && (
@@ -242,20 +253,32 @@ const ToolDisplay = ({ call, result, status = "requested", isError = false, isDe
242253
243254 < div className = "mt-4 w-full" >
244255 { status === "executing" && ! hasResult && (
245- < div className = "flex items-center gap-2 py-1 " >
256+ < div className = "flex items-center gap-2 py-2 " >
246257 < Loader2 className = "h-4 w-4 animate-spin" />
247258 < span className = "text-sm" > Executing...</ span >
248259 </ div >
249260 ) }
250- { hasResult && resultContent && (
251- < CollapsibleSection
252- icon = { Text }
253- expanded = { areResultsExpanded }
254- onToggle = { ( ) => setAreResultsExpanded ( ! areResultsExpanded ) }
255- previewContent = { resultContent }
256- expandedContent = { resultContent }
257- errorStyle = { isError }
258- />
261+ { hasResult && (
262+ < >
263+ < Button variant = "ghost" size = "sm" className = "p-0 h-auto justify-start" onClick = { ( ) => setAreResultsExpanded ( ! areResultsExpanded ) } >
264+ < Text className = "w-4 h-4 mr-2" />
265+ < span className = "mr-2" > { isError ? "Error" : "Results" } </ span >
266+ { areResultsExpanded ? < ChevronUp className = "w-4 h-4 ml-auto" /> : < ChevronDown className = "w-4 h-4 ml-auto" /> }
267+ </ Button >
268+ { areResultsExpanded && (
269+ < div className = "relative" >
270+ < ScrollArea className = { `max-h-96 overflow-y-auto p-4 w-full mt-2 ${ isError ? 'bg-red-50 dark:bg-red-950/10' : '' } ` } >
271+ < pre className = { `text-sm whitespace-pre-wrap break-words ${ isError ? 'text-red-600 dark:text-red-400' : '' } ` } >
272+ { result . content }
273+ </ pre >
274+ </ ScrollArea >
275+
276+ < Button variant = "ghost" size = "sm" className = "absolute top-2 right-2 p-2" onClick = { handleCopy } >
277+ { isCopied ? < Check className = "w-4 h-4" /> : < Copy className = "w-4 h-4" /> }
278+ </ Button >
279+ </ div >
280+ ) }
281+ </ >
259282 ) }
260283 </ div >
261284 </ CardContent >
0 commit comments