@@ -4,27 +4,30 @@ import { useState } from "react"
44import type { UIMessage } from "@ai-sdk/react"
55import { Streamdown } from "streamdown"
66import {
7+ BookOpenIcon ,
78 ChevronDownIcon ,
89 ChevronRightIcon ,
9- Loader2 ,
10- SearchIcon ,
11- GlobeIcon ,
12- PlusIcon ,
13- BookOpenIcon ,
1410 ClockIcon ,
11+ GlobeIcon ,
1512 ListIcon ,
16- XCircleIcon ,
13+ Loader2 ,
14+ PlusIcon ,
15+ SearchIcon ,
16+ TerminalIcon ,
1717 WrenchIcon ,
18+ XCircleIcon ,
1819} from "lucide-react"
1920import { cn } from "@lib/utils"
2021import { isWebSearchToolName } from "@/lib/chat-web-search-tools"
2122import { RelatedMemories } from "./related-memories"
2223import { MessageActions } from "./message-actions"
2324
2425const TOOL_META : Record < string , { label : string ; icon : typeof SearchIcon } > = {
25- searchMemories : { label : "Search Memories " , icon : SearchIcon } ,
26+ bash : { label : "Memory " , icon : TerminalIcon } ,
2627 web_search : { label : "Web search" , icon : GlobeIcon } ,
2728 google_search : { label : "Google search" , icon : GlobeIcon } ,
29+ // legacy tool names kept for existing persisted messages
30+ searchMemories : { label : "Search Memories" , icon : SearchIcon } ,
2831 addMemory : { label : "Add Memory" , icon : PlusIcon } ,
2932 fetchMemory : { label : "Fetch Memory" , icon : BookOpenIcon } ,
3033 scheduleTask : { label : "Schedule Task" , icon : ClockIcon } ,
@@ -95,9 +98,119 @@ function WebSourcesGroup({ sources }: { sources: SourceUrlPart[] }) {
9598 )
9699}
97100
101+ function BashToolDisplay ( { part } : { part : ToolCallDisplayPart } ) {
102+ const [ expanded , setExpanded ] = useState ( false )
103+ const isLoading =
104+ part . state === "input-streaming" || part . state === "input-available"
105+ const isDone = part . state === "output-available"
106+ const isError = part . state === "error" || part . state === "output-error"
107+
108+ const cmd =
109+ part . input && typeof part . input === "object" && "cmd" in part . input
110+ ? String ( ( part . input as { cmd : string } ) . cmd )
111+ : undefined
112+
113+ const output =
114+ isDone && part . output && typeof part . output === "object"
115+ ? ( part . output as { stdout ?: string ; stderr ?: string ; exitCode ?: number } )
116+ : undefined
117+
118+ const hasOutput =
119+ output &&
120+ ( ( output . stdout && output . stdout . length > 0 ) ||
121+ ( output . stderr && output . stderr . length > 0 ) )
122+ const errorText = part . errorText
123+ const hasExpandable = hasOutput || ( isError && errorText )
124+
125+ return (
126+ < div className = "rounded-lg border border-[#1E2128] bg-[#0D121A] text-xs my-1 overflow-hidden font-mono" >
127+ < button
128+ type = "button"
129+ onClick = { ( ) => setExpanded ( ! expanded ) }
130+ className = { cn (
131+ "flex items-center gap-2 w-full px-3 py-2 cursor-pointer hover:bg-[#141922] transition-colors" ,
132+ expanded && hasExpandable && "border-b border-[#1E2128]" ,
133+ ) }
134+ >
135+ { isLoading ? (
136+ < Loader2 className = "size-3 animate-spin text-blue-400 shrink-0" />
137+ ) : (
138+ < TerminalIcon
139+ className = { cn (
140+ "size-3 shrink-0" ,
141+ isDone
142+ ? output ?. exitCode === 0
143+ ? "text-emerald-400"
144+ : "text-amber-400"
145+ : isError
146+ ? "text-red-400"
147+ : "text-white/50" ,
148+ ) }
149+ />
150+ ) }
151+ < span className = { cn ( "text-white/50" , isLoading && "text-blue-400/60" ) } >
152+ $
153+ </ span >
154+ < span
155+ className = { cn (
156+ "flex-1 text-left truncate" ,
157+ isDone
158+ ? output ?. exitCode === 0
159+ ? "text-emerald-300"
160+ : "text-amber-300"
161+ : isLoading
162+ ? "text-blue-300"
163+ : isError
164+ ? "text-red-300"
165+ : "text-white/70" ,
166+ ) }
167+ >
168+ { cmd ?? "..." }
169+ </ span >
170+ { isLoading && (
171+ < span className = "text-white/30 shrink-0" > running...</ span >
172+ ) }
173+ { isDone && ! hasOutput && (
174+ < span className = "text-white/30 shrink-0" > done</ span >
175+ ) }
176+ { isError && < span className = "text-red-400/60 shrink-0" > error</ span > }
177+ { hasExpandable &&
178+ ( expanded ? (
179+ < ChevronDownIcon className = "size-3 text-white/30 shrink-0" />
180+ ) : (
181+ < ChevronRightIcon className = "size-3 text-white/30 shrink-0" />
182+ ) ) }
183+ </ button >
184+
185+ { expanded && ( hasOutput || ( isError && errorText ) ) && (
186+ < div className = "px-3 py-2 space-y-1" >
187+ { output ?. stdout && output . stdout . length > 0 && (
188+ < pre className = "text-white/70 bg-[#080B10] rounded p-2 overflow-x-auto max-h-48 overflow-y-auto whitespace-pre-wrap break-all text-[11px]" >
189+ { output . stdout }
190+ </ pre >
191+ ) }
192+ { output ?. stderr && output . stderr . length > 0 && (
193+ < pre className = "text-amber-300/70 bg-[#080B10] rounded p-2 overflow-x-auto max-h-24 overflow-y-auto whitespace-pre-wrap break-all text-[11px]" >
194+ { output . stderr }
195+ </ pre >
196+ ) }
197+ { isError && errorText && (
198+ < pre className = "text-red-300/90 bg-[#080B10] rounded p-2 overflow-x-auto max-h-24 overflow-y-auto whitespace-pre-wrap break-all text-[11px]" >
199+ { errorText }
200+ </ pre >
201+ ) }
202+ </ div >
203+ ) }
204+ </ div >
205+ )
206+ }
207+
98208function ToolCallDisplay ( { part } : { part : ToolCallDisplayPart } ) {
99209 const [ expanded , setExpanded ] = useState ( false )
100210 const toolName = part . type . replace ( "tool-" , "" )
211+ if ( toolName === "bash" ) {
212+ return < BashToolDisplay part = { part } />
213+ }
101214 const meta =
102215 TOOL_META [ toolName ] ??
103216 ( isWebSearchToolName ( toolName )
0 commit comments