22// SPDX-License-Identifier: Apache-2.0
33
44import { WaveStreamdown } from "@/app/element/streamdown" ;
5+ import { RpcApi } from "@/app/store/wshclientapi" ;
6+ import { TabRpcClient } from "@/app/store/wshrpcutil" ;
57import { cn } from "@/util/util" ;
6- import { useAtomValue } from "jotai" ;
7- import { memo } from "react" ;
8+ import { memo , useEffect , useState } from "react" ;
89import { getFileIcon } from "./ai-utils" ;
910import { WaveUIMessage , WaveUIMessagePart } from "./aitypes" ;
1011import { WaveAIModel } from "./waveai-model" ;
@@ -67,6 +68,84 @@ const UserMessageFiles = memo(({ fileParts }: UserMessageFilesProps) => {
6768
6869UserMessageFiles . displayName = "UserMessageFiles" ;
6970
71+ interface AIToolUseProps {
72+ part : WaveUIMessagePart & { type : "data-tooluse" } ;
73+ isStreaming : boolean ;
74+ }
75+
76+ const AIToolUse = memo ( ( { part } : AIToolUseProps ) => {
77+ const toolData = part . data ;
78+ const [ userApprovalOverride , setUserApprovalOverride ] = useState < string | null > ( null ) ;
79+
80+ const statusIcon = toolData . status === "completed" ? "✓" : toolData . status === "error" ? "✗" : "•" ;
81+ const statusColor =
82+ toolData . status === "completed"
83+ ? "text-green-400"
84+ : toolData . status === "error"
85+ ? "text-red-400"
86+ : "text-gray-400" ;
87+
88+ const effectiveApproval = userApprovalOverride || toolData . approval ;
89+
90+ useEffect ( ( ) => {
91+ if ( effectiveApproval !== "needs-approval" ) return ;
92+
93+ const interval = setInterval ( ( ) => {
94+ RpcApi . WaveAIToolApproveCommand ( TabRpcClient , {
95+ toolcallid : toolData . toolcallid ,
96+ keepalive : true ,
97+ } ) ;
98+ } , 4000 ) ;
99+
100+ return ( ) => clearInterval ( interval ) ;
101+ } , [ effectiveApproval , toolData . toolcallid ] ) ;
102+
103+ const handleApprove = ( ) => {
104+ setUserApprovalOverride ( "user-approved" ) ;
105+ RpcApi . WaveAIToolApproveCommand ( TabRpcClient , {
106+ toolcallid : toolData . toolcallid ,
107+ approval : "user-approved" ,
108+ } ) ;
109+ } ;
110+
111+ const handleDeny = ( ) => {
112+ setUserApprovalOverride ( "user-denied" ) ;
113+ RpcApi . WaveAIToolApproveCommand ( TabRpcClient , {
114+ toolcallid : toolData . toolcallid ,
115+ approval : "user-denied" ,
116+ } ) ;
117+ } ;
118+
119+ return (
120+ < div className = { cn ( "flex items-start gap-2 p-2 rounded bg-gray-800 border border-gray-700" , statusColor ) } >
121+ < span className = "font-bold" > { statusIcon } </ span >
122+ < div className = "flex-1" >
123+ < div className = "font-semibold" > { toolData . toolname } </ div >
124+ { toolData . tooldesc && < div className = "text-sm text-gray-400" > { toolData . tooldesc } </ div > }
125+ { toolData . errormessage && < div className = "text-sm text-red-300 mt-1" > { toolData . errormessage } </ div > }
126+ { effectiveApproval === "needs-approval" && (
127+ < div className = "mt-2 flex gap-2" >
128+ < button
129+ onClick = { handleApprove }
130+ className = "px-3 py-1 border border-gray-600 text-gray-300 hover:border-gray-500 hover:text-white text-sm rounded cursor-pointer transition-colors"
131+ >
132+ Approve
133+ </ button >
134+ < button
135+ onClick = { handleDeny }
136+ className = "px-3 py-1 border border-gray-600 text-gray-300 hover:border-gray-500 hover:text-white text-sm rounded cursor-pointer transition-colors"
137+ >
138+ Deny
139+ </ button >
140+ </ div >
141+ ) }
142+ </ div >
143+ </ div >
144+ ) ;
145+ } ) ;
146+
147+ AIToolUse . displayName = "AIToolUse" ;
148+
70149interface AIMessagePartProps {
71150 part : WaveUIMessagePart ;
72151 role : string ;
@@ -93,9 +172,8 @@ const AIMessagePart = memo(({ part, role, isStreaming }: AIMessagePartProps) =>
93172 }
94173 }
95174
96- if ( part . type . startsWith ( "tool-" ) && "state" in part && part . state === "input-available" ) {
97- const toolName = part . type . substring ( 5 ) ; // Remove "tool-" prefix
98- return < div className = "text-gray-400 italic" > Calling tool { toolName } </ div > ;
175+ if ( part . type === "data-tooluse" && part . data ) {
176+ return < AIToolUse part = { part as WaveUIMessagePart & { type : "data-tooluse" } } isStreaming = { isStreaming } /> ;
99177 }
100178
101179 return null ;
@@ -110,7 +188,9 @@ interface AIMessageProps {
110188
111189const isDisplayPart = ( part : WaveUIMessagePart ) : boolean => {
112190 return (
113- part . type === "text" || ( part . type . startsWith ( "tool-" ) && "state" in part && part . state === "input-available" )
191+ part . type === "text" ||
192+ part . type === "data-tooluse" ||
193+ ( part . type . startsWith ( "tool-" ) && "state" in part && part . state === "input-available" )
114194 ) ;
115195} ;
116196
@@ -122,7 +202,10 @@ export const AIMessage = memo(({ message, isStreaming }: AIMessageProps) => {
122202 ) ;
123203 const hasContent =
124204 displayParts . length > 0 &&
125- displayParts . some ( ( part ) => ( part . type === "text" && part . text ) || part . type . startsWith ( "tool-" ) ) ;
205+ displayParts . some (
206+ ( part ) =>
207+ ( part . type === "text" && part . text ) || part . type . startsWith ( "tool-" ) || part . type === "data-tooluse"
208+ ) ;
126209
127210 const showThinkingOnly = ! hasContent && isStreaming && message . role === "assistant" ;
128211 const showThinkingInline = hasContent && isStreaming && message . role === "assistant" ;
0 commit comments