11"use client" ;
22
33import { useChat } from "@ai-sdk/react" ;
4+ import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai" ;
45import type { ChatUiMessage } from "@/lib/chat-tools" ;
56import type { TriggerChatTransport } from "@trigger.dev/sdk/chat" ;
67import type { CompactionChunkData } from "@trigger.dev/sdk/ai" ;
@@ -9,7 +10,15 @@ import { useCallback, useEffect, useRef, useState } from "react";
910import { Streamdown } from "streamdown" ;
1011import { MODEL_OPTIONS } from "@/lib/models" ;
1112
12- function ToolInvocation ( { part } : { part : any } ) {
13+ function ToolInvocation ( {
14+ part,
15+ onApprove,
16+ onDeny,
17+ } : {
18+ part : any ;
19+ onApprove ?: ( approvalId : string ) => void ;
20+ onDeny ?: ( approvalId : string ) => void ;
21+ } ) {
1322 const [ expanded , setExpanded ] = useState ( false ) ;
1423 const toolName = part . type . startsWith ( "tool-" ) ? part . type . slice ( 5 ) : "tool" ;
1524 const state = part . state ?? "input-available" ;
@@ -18,6 +27,9 @@ function ToolInvocation({ part }: { part: any }) {
1827
1928 const isLoading = state === "input-streaming" || state === "input-available" ;
2029 const isError = state === "output-error" ;
30+ const needsApproval = state === "approval-requested" ;
31+ const wasApproved = state === "approval-responded" && part . approval ?. approved === true ;
32+ const wasDenied = state === "approval-responded" && part . approval ?. approved === false ;
2133
2234 return (
2335 < div className = "my-1 rounded border border-gray-200 bg-gray-50 text-xs" >
@@ -29,12 +41,37 @@ function ToolInvocation({ part }: { part: any }) {
2941 { isLoading && (
3042 < span className = "inline-block h-3 w-3 animate-spin rounded-full border-2 border-gray-300 border-t-gray-600" />
3143 ) }
32- { ! isLoading && ! isError && < span className = "text-green-600" > ✓</ span > }
44+ { needsApproval && < span className = "text-amber-500" > ⚠</ span > }
45+ { wasApproved && < span className = "text-green-600" > ✓</ span > }
46+ { wasDenied && < span className = "text-red-600" > ✗</ span > }
47+ { ! isLoading && ! needsApproval && ! wasApproved && ! wasDenied && ! isError && (
48+ < span className = "text-green-600" > ✓</ span >
49+ ) }
3350 { isError && < span className = "text-red-600" > ✗</ span > }
3451 < span > { toolName } </ span >
52+ { needsApproval && < span className = "text-amber-500 text-[10px]" > needs approval</ span > }
3553 < span className = "ml-auto text-gray-400" > { expanded ? "▲" : "▼" } </ span >
3654 </ button >
3755
56+ { needsApproval && (
57+ < div className = "flex gap-2 border-t border-gray-200 px-3 py-2" >
58+ < button
59+ type = "button"
60+ onClick = { ( ) => onApprove ?.( part . approval . id ) }
61+ className = "rounded bg-green-600 px-3 py-1 text-xs font-medium text-white hover:bg-green-700"
62+ >
63+ Approve
64+ </ button >
65+ < button
66+ type = "button"
67+ onClick = { ( ) => onDeny ?.( part . approval . id ) }
68+ className = "rounded bg-red-600 px-3 py-1 text-xs font-medium text-white hover:bg-red-700"
69+ >
70+ Deny
71+ </ button >
72+ </ div >
73+ ) }
74+
3875 { expanded && (
3976 < div className = "border-t border-gray-200 px-3 py-2 space-y-2" >
4077 { args && Object . keys ( args ) . length > 0 && (
@@ -267,11 +304,20 @@ export function Chat({
267304 const turnCounter = useRef ( 0 ) ;
268305 const [ ttfbHistory , setTtfbHistory ] = useState < TtfbEntry [ ] > ( [ ] ) ;
269306
270- const { messages, setMessages, sendMessage, stop : aiStop , status, error } = useChat ( {
307+ const {
308+ messages,
309+ setMessages,
310+ sendMessage,
311+ stop : aiStop ,
312+ addToolApprovalResponse,
313+ status,
314+ error,
315+ } = useChat ( {
271316 id : chatId ,
272317 messages : initialMessages ,
273318 transport,
274319 resume : resumeProp ,
320+ sendAutomaticallyWhen : lastAssistantMessageIsCompleteWithApprovalResponses ,
275321 } ) ;
276322
277323 // Use transport.stopGeneration for reliable stop after reconnect.
@@ -282,6 +328,21 @@ export function Chat({
282328 aiStop ( ) ;
283329 } , [ transport , chatId , aiStop ] ) ;
284330
331+ // Tool approval callbacks
332+ const handleApprove = useCallback (
333+ ( approvalId : string ) => {
334+ addToolApprovalResponse ( { id : approvalId , approved : true } ) ;
335+ } ,
336+ [ addToolApprovalResponse , chatId , messages , status ]
337+ ) ;
338+
339+ const handleDeny = useCallback (
340+ ( approvalId : string ) => {
341+ addToolApprovalResponse ( { id : approvalId , approved : false , reason : "User denied" } ) ;
342+ } ,
343+ [ addToolApprovalResponse , chatId ]
344+ ) ;
345+
285346 // Notify parent of first user message (for chat metadata creation)
286347 useEffect ( ( ) => {
287348 if ( hasCalledFirstMessage . current ) return ;
@@ -549,7 +610,14 @@ export function Chat({
549610 }
550611
551612 if ( part . type . startsWith ( "tool-" ) ) {
552- return < ToolInvocation key = { i } part = { part } /> ;
613+ return (
614+ < ToolInvocation
615+ key = { i }
616+ part = { part }
617+ onApprove = { handleApprove }
618+ onDeny = { handleDeny }
619+ />
620+ ) ;
553621 }
554622
555623 if ( pending . isInjectionPoint ( part ) ) {
0 commit comments