11import type { ToolCallMessagePartComponent , ToolCallMessagePartProps } from "@assistant-ui/react" ;
2+ import { bindFirst } from "@geekist/llm-core" ;
23import { CheckIcon , ChevronDownIcon , ChevronUpIcon , XCircleIcon } from "lucide-react" ;
3- import { useState } from "react" ;
4+ import type { Dispatch , SetStateAction } from "react" ;
5+ import { useId , useState } from "react" ;
46import { Button } from "../ui/button" ;
57import { cn } from "../../lib/utils" ;
68
79type ToolFallbackProps = ToolCallMessagePartProps ;
810
11+ const applyToggleCollapsed = ( value : boolean ) => ! value ;
12+
13+ const toggleCollapsed = ( setIsCollapsed : Dispatch < SetStateAction < boolean > > ) => {
14+ setIsCollapsed ( applyToggleCollapsed ) ;
15+ return true ;
16+ } ;
17+
18+ const safeStringify = ( value : unknown ) => {
19+ if ( typeof value === "string" ) {
20+ return value ;
21+ }
22+ try {
23+ return JSON . stringify ( value ) ;
24+ } catch {
25+ return String ( value ) ;
26+ }
27+ } ;
28+
29+ const safeStringifyPretty = ( value : unknown ) => {
30+ if ( typeof value === "string" ) {
31+ return value ;
32+ }
33+ try {
34+ return JSON . stringify ( value , null , 2 ) ;
35+ } catch {
36+ return safeStringify ( value ) ;
37+ }
38+ } ;
39+
940const ToolFallbackImpl = ( { toolName, argsText, result, status } : ToolFallbackProps ) => {
1041 const [ isCollapsed , setIsCollapsed ] = useState ( true ) ;
42+ const contentId = useId ( ) ;
43+ const handleToggleCollapsed = bindFirst ( toggleCollapsed , setIsCollapsed ) ;
1144
1245 const isCancelled = status ?. type === "incomplete" && status . reason === "cancelled" ;
13- const cancelledReason =
14- isCancelled && status . error
15- ? typeof status . error === "string"
16- ? status . error
17- : JSON . stringify ( status . error )
18- : null ;
46+ const cancelledReason = isCancelled && status . error ? safeStringify ( status . error ) : null ;
1947
2048 return (
2149 < div
@@ -40,21 +68,18 @@ const ToolFallbackImpl = ({ toolName, argsText, result, status }: ToolFallbackPr
4068 < b > { toolName } </ b >
4169 </ p >
4270 < Button
43- onClick = { ( ) => setIsCollapsed ( ! isCollapsed ) }
71+ onClick = { handleToggleCollapsed }
4472 size = "icon"
4573 variant = "ghost"
4674 aria-expanded = { ! isCollapsed }
47- aria-controls = "tool-fallback-content"
75+ aria-controls = { contentId }
4876 aria-label = { isCollapsed ? "Expand tool details" : "Collapse tool details" }
4977 >
5078 { isCollapsed ? < ChevronUpIcon /> : < ChevronDownIcon /> }
5179 </ Button >
5280 </ div >
5381 { ! isCollapsed && (
54- < div
55- id = "tool-fallback-content"
56- className = "aui-tool-fallback-content flex flex-col gap-2 border-t pt-2"
57- >
82+ < div id = { contentId } className = "aui-tool-fallback-content flex flex-col gap-2 border-t pt-2" >
5883 { cancelledReason && (
5984 < div className = "aui-tool-fallback-cancelled-root px-4" >
6085 < p className = "aui-tool-fallback-cancelled-header font-semibold text-muted-foreground" >
@@ -72,7 +97,7 @@ const ToolFallbackImpl = ({ toolName, argsText, result, status }: ToolFallbackPr
7297 < div className = "aui-tool-fallback-result-root border-t border-dashed px-4 pt-2" >
7398 < p className = "aui-tool-fallback-result-header font-semibold" > Result:</ p >
7499 < pre className = "aui-tool-fallback-result-content whitespace-pre-wrap" >
75- { typeof result === "string" ? result : JSON . stringify ( result , null , 2 ) }
100+ { safeStringifyPretty ( result ) }
76101 </ pre >
77102 </ div >
78103 ) }
0 commit comments