1+ import React , { useState } from "react" ;
2+ import { motion , AnimatePresence } from "framer-motion" ;
3+ import {
4+ ChevronDown ,
5+ ChevronRight ,
6+ Loader2 ,
7+ CheckCircle2 ,
8+ AlertCircle ,
9+ Terminal ,
10+ FileText ,
11+ Search ,
12+ Edit ,
13+ FolderOpen ,
14+ Code
15+ } from "lucide-react" ;
16+ import { cn } from "@/lib/utils" ;
17+ import type { ToolCall , ToolResult } from "@/types/enhanced-messages" ;
18+
19+ interface CollapsibleToolResultProps {
20+ toolCall : ToolCall ;
21+ toolResult ?: ToolResult ;
22+ className ?: string ;
23+ children ?: React . ReactNode ;
24+ }
25+
26+ // Map tool names to icons
27+ const toolIcons : Record < string , React . ReactNode > = {
28+ read : < FileText className = "h-4 w-4" /> ,
29+ write : < Edit className = "h-4 w-4" /> ,
30+ edit : < Edit className = "h-4 w-4" /> ,
31+ multiedit : < Edit className = "h-4 w-4" /> ,
32+ bash : < Terminal className = "h-4 w-4" /> ,
33+ ls : < FolderOpen className = "h-4 w-4" /> ,
34+ glob : < Search className = "h-4 w-4" /> ,
35+ grep : < Search className = "h-4 w-4" /> ,
36+ task : < Code className = "h-4 w-4" /> ,
37+ default : < Terminal className = "h-4 w-4" />
38+ } ;
39+
40+ // Get tool icon based on tool name
41+ function getToolIcon ( toolName : string ) : React . ReactNode {
42+ const lowerName = toolName . toLowerCase ( ) ;
43+ return toolIcons [ lowerName ] || toolIcons . default ;
44+ }
45+
46+ // Get display name for tools
47+ function getToolDisplayName ( toolName : string ) : string {
48+ const displayNames : Record < string , string > = {
49+ ls : "List directory" ,
50+ read : "Read file" ,
51+ write : "Write file" ,
52+ edit : "Edit file" ,
53+ multiedit : "Multi-edit file" ,
54+ bash : "Run command" ,
55+ glob : "Find files" ,
56+ grep : "Search files" ,
57+ task : "Run task" ,
58+ todowrite : "Update todos" ,
59+ todoread : "Read todos" ,
60+ websearch : "Search web" ,
61+ webfetch : "Fetch webpage"
62+ } ;
63+
64+ const lowerName = toolName . toLowerCase ( ) ;
65+ return displayNames [ lowerName ] || toolName ;
66+ }
67+
68+ // Get a brief description of the tool call
69+ function getToolDescription ( toolCall : ToolCall ) : string {
70+ const name = toolCall . name . toLowerCase ( ) ;
71+ const input = toolCall . input ;
72+
73+ switch ( name ) {
74+ case "read" :
75+ return input ?. file_path ? `${ input . file_path } ` : "Reading file" ;
76+ case "write" :
77+ return input ?. file_path ? `${ input . file_path } ` : "Writing file" ;
78+ case "edit" :
79+ case "multiedit" :
80+ return input ?. file_path ? `${ input . file_path } ` : "Editing file" ;
81+ case "bash" :
82+ return input ?. command ? `${ input . command } ` : "Running command" ;
83+ case "ls" :
84+ return input ?. path ? `${ input . path } ` : "Listing directory" ;
85+ case "glob" :
86+ return input ?. pattern ? `${ input . pattern } ` : "Finding files" ;
87+ case "grep" :
88+ return input ?. pattern ? `${ input . pattern } ` : "Searching files" ;
89+ case "task" :
90+ return input ?. description || "Running task" ;
91+ default :
92+ return toolCall . name ;
93+ }
94+ }
95+
96+ export const CollapsibleToolResult : React . FC < CollapsibleToolResultProps > = ( {
97+ toolCall,
98+ toolResult,
99+ className,
100+ children
101+ } ) => {
102+ const [ isExpanded , setIsExpanded ] = useState ( false ) ;
103+ const isPending = ! toolResult ;
104+ const isError = toolResult ?. isError ;
105+
106+ return (
107+ < div className = { cn ( "space-y-2" , className ) } >
108+ { /* Tool Call Header */ }
109+ < div
110+ className = { cn (
111+ "flex items-center gap-2 p-2 rounded-md border cursor-pointer transition-colors" ,
112+ "hover:bg-muted/50" ,
113+ isPending && "border-muted-foreground/20" ,
114+ ! isPending && ! isError && "border-green-500/20" ,
115+ isError && "border-destructive/20"
116+ ) }
117+ onClick = { ( ) => setIsExpanded ( ! isExpanded ) }
118+ >
119+ { /* Expand/Collapse Icon */ }
120+ < motion . div
121+ animate = { { rotate : isExpanded ? 90 : 0 } }
122+ transition = { { duration : 0.2 } }
123+ >
124+ < ChevronRight className = "h-3 w-3 text-muted-foreground" />
125+ </ motion . div >
126+
127+ { /* Tool Icon */ }
128+ < div className = "text-muted-foreground" >
129+ { getToolIcon ( toolCall . name ) }
130+ </ div >
131+
132+ { /* Tool Name */ }
133+ < span className = "text-sm font-medium" >
134+ { getToolDisplayName ( toolCall . name ) }
135+ </ span >
136+
137+ { /* Tool Description */ }
138+ < span className = "text-xs text-muted-foreground flex-1 truncate" >
139+ { getToolDescription ( toolCall ) }
140+ </ span >
141+
142+ { /* Status Icon */ }
143+ < div className = "ml-auto" >
144+ { isPending ? (
145+ < Loader2 className = "h-4 w-4 animate-spin text-muted-foreground" />
146+ ) : isError ? (
147+ < AlertCircle className = "h-4 w-4 text-destructive" />
148+ ) : (
149+ < CheckCircle2 className = "h-4 w-4 text-green-500" />
150+ ) }
151+ </ div >
152+ </ div >
153+
154+ { /* Tool Result (collapsible) */ }
155+ < AnimatePresence >
156+ { isExpanded && toolResult && (
157+ < motion . div
158+ initial = { { height : 0 , opacity : 0 } }
159+ animate = { { height : "auto" , opacity : 1 } }
160+ exit = { { height : 0 , opacity : 0 } }
161+ transition = { { duration : 0.2 } }
162+ className = "overflow-hidden"
163+ >
164+ < div className = { cn (
165+ "ml-6 p-2 rounded-md border" ,
166+ isError ? "border-destructive/20 bg-destructive/5" : "border-green-500/20 bg-green-500/5"
167+ ) } >
168+ < div className = "flex items-center gap-2 mb-2" >
169+ { isError ? (
170+ < AlertCircle className = "h-4 w-4 text-destructive" />
171+ ) : (
172+ < CheckCircle2 className = "h-4 w-4 text-green-500" />
173+ ) }
174+ < span className = "text-sm font-medium" >
175+ { isError ? "Tool Error" : "Tool Result" }
176+ </ span >
177+ </ div >
178+
179+ { /* Result Content */ }
180+ < div className = "text-xs font-mono overflow-x-auto whitespace-pre-wrap" >
181+ { typeof toolResult . content === 'string'
182+ ? toolResult . content
183+ : JSON . stringify ( toolResult . content , null , 2 ) }
184+ </ div >
185+ </ div >
186+ </ motion . div >
187+ ) }
188+ </ AnimatePresence >
189+ </ div >
190+ ) ;
191+ } ;
0 commit comments