@@ -237,6 +237,16 @@ function isSpecialToolCall(toolCall: CopilotToolCall): boolean {
237237 return workflowOperationTools . includes ( toolCall . name )
238238}
239239
240+ /**
241+ * Checks if a tool is an integration tool (server-side executed, not a client tool)
242+ */
243+ function isIntegrationTool ( toolName : string ) : boolean {
244+ // Check if it's NOT a client tool (not in CLASS_TOOL_METADATA and not in registered tools)
245+ const isClientTool = ! ! CLASS_TOOL_METADATA [ toolName ]
246+ const isRegisteredTool = ! ! getRegisteredTools ( ) [ toolName ]
247+ return ! isClientTool && ! isRegisteredTool
248+ }
249+
240250function shouldShowRunSkipButtons ( toolCall : CopilotToolCall ) : boolean {
241251 const instance = getClientTool ( toolCall . id )
242252 let hasInterrupt = ! ! instance ?. getInterruptDisplays ?.( )
@@ -251,7 +261,19 @@ function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean {
251261 }
252262 } catch { }
253263 }
254- return hasInterrupt && toolCall . state === 'pending'
264+
265+ // Show buttons for client tools with interrupts
266+ if ( hasInterrupt && toolCall . state === 'pending' ) {
267+ return true
268+ }
269+
270+ // Also show buttons for integration tools in pending state (they need user confirmation)
271+ const mode = useCopilotStore . getState ( ) . mode
272+ if ( mode === 'build' && isIntegrationTool ( toolCall . name ) && toolCall . state === 'pending' ) {
273+ return true
274+ }
275+
276+ return false
255277}
256278
257279async function handleRun (
@@ -261,6 +283,18 @@ async function handleRun(
261283 editedParams ?: any
262284) {
263285 const instance = getClientTool ( toolCall . id )
286+
287+ // Handle integration tools (server-side execution)
288+ if ( ! instance && isIntegrationTool ( toolCall . name ) ) {
289+ try {
290+ onStateChange ?.( 'executing' )
291+ await useCopilotStore . getState ( ) . executeIntegrationTool ( toolCall . id )
292+ } catch ( e ) {
293+ setToolCallState ( toolCall , 'errored' , { error : e instanceof Error ? e . message : String ( e ) } )
294+ }
295+ return
296+ }
297+
264298 if ( ! instance ) return
265299 try {
266300 const mergedParams =
@@ -278,6 +312,27 @@ async function handleRun(
278312
279313async function handleSkip ( toolCall : CopilotToolCall , setToolCallState : any , onStateChange ?: any ) {
280314 const instance = getClientTool ( toolCall . id )
315+
316+ // Handle integration tools (skip by marking as rejected and notifying backend)
317+ if ( ! instance && isIntegrationTool ( toolCall . name ) ) {
318+ setToolCallState ( toolCall , 'rejected' )
319+ onStateChange ?.( 'rejected' )
320+ // Notify backend that tool was skipped
321+ try {
322+ await fetch ( '/api/copilot/tools/mark-complete' , {
323+ method : 'POST' ,
324+ headers : { 'Content-Type' : 'application/json' } ,
325+ body : JSON . stringify ( {
326+ id : toolCall . id ,
327+ name : toolCall . name ,
328+ status : 400 ,
329+ message : 'Tool execution skipped by user' ,
330+ } ) ,
331+ } )
332+ } catch { }
333+ return
334+ }
335+
281336 if ( instance ) {
282337 try {
283338 await instance . handleReject ?.( )
@@ -346,11 +401,15 @@ function RunSkipButtons({
346401} ) {
347402 const [ isProcessing , setIsProcessing ] = useState ( false )
348403 const [ buttonsHidden , setButtonsHidden ] = useState ( false )
349- const { setToolCallState } = useCopilotStore ( )
404+ const { setToolCallState, addAutoAllowedTool } = useCopilotStore ( )
350405
351406 const instance = getClientTool ( toolCall . id )
352407 const interruptDisplays = instance ?. getInterruptDisplays ?.( )
353- const acceptLabel = interruptDisplays ?. accept ?. text || 'Run'
408+ const isIntegration = isIntegrationTool ( toolCall . name )
409+
410+ // For integration tools: Allow, Always Allow, Skip
411+ // For client tools with interrupts: Run, Skip (or custom labels)
412+ const acceptLabel = isIntegration ? 'Allow' : interruptDisplays ?. accept ?. text || 'Run'
354413 const rejectLabel = interruptDisplays ?. reject ?. text || 'Skip'
355414
356415 const onRun = async ( ) => {
@@ -363,6 +422,19 @@ function RunSkipButtons({
363422 }
364423 }
365424
425+ const onAlwaysAllow = async ( ) => {
426+ setIsProcessing ( true )
427+ setButtonsHidden ( true )
428+ try {
429+ // Add to auto-allowed list
430+ await addAutoAllowedTool ( toolCall . name )
431+ // Then execute
432+ await handleRun ( toolCall , setToolCallState , onStateChange , editedParams )
433+ } finally {
434+ setIsProcessing ( false )
435+ }
436+ }
437+
366438 if ( buttonsHidden ) return null
367439
368440 return (
@@ -371,6 +443,12 @@ function RunSkipButtons({
371443 { isProcessing ? < Loader2 className = 'mr-1 h-3 w-3 animate-spin' /> : null }
372444 { acceptLabel }
373445 </ Button >
446+ { isIntegration && (
447+ < Button onClick = { onAlwaysAllow } disabled = { isProcessing } variant = 'default' >
448+ { isProcessing ? < Loader2 className = 'mr-1 h-3 w-3 animate-spin' /> : null }
449+ Always Allow
450+ </ Button >
451+ ) }
374452 < Button
375453 onClick = { async ( ) => {
376454 setButtonsHidden ( true )
@@ -402,12 +480,17 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
402480 toolCall . name === 'run_workflow' )
403481
404482 const [ expanded , setExpanded ] = useState ( isExpandablePending )
483+ const [ showRemoveAutoAllow , setShowRemoveAutoAllow ] = useState ( false )
405484
406485 // State for editable parameters
407486 const params = ( toolCall as any ) . parameters || ( toolCall as any ) . input || toolCall . params || { }
408487 const [ editedParams , setEditedParams ] = useState ( params )
409488 const paramsRef = useRef ( params )
410489
490+ // Check if this integration tool is auto-allowed
491+ const { isToolAutoAllowed, removeAutoAllowedTool } = useCopilotStore ( )
492+ const isAutoAllowed = isIntegrationTool ( toolCall . name ) && isToolAutoAllowed ( toolCall . name )
493+
411494 // Update edited params when toolCall params change (deep comparison to avoid resetting user edits on ref change)
412495 useEffect ( ( ) => {
413496 if ( JSON . stringify ( params ) !== JSON . stringify ( paramsRef . current ) ) {
@@ -846,14 +929,20 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
846929 )
847930 }
848931
932+ // Determine if tool name should be clickable (expandable tools or auto-allowed integration tools)
933+ const isToolNameClickable = isExpandableTool || isAutoAllowed
934+
935+ const handleToolNameClick = ( ) => {
936+ if ( isExpandableTool ) {
937+ setExpanded ( ( e ) => ! e )
938+ } else if ( isAutoAllowed ) {
939+ setShowRemoveAutoAllow ( ( prev ) => ! prev )
940+ }
941+ }
942+
849943 return (
850944 < div className = 'w-full' >
851- < div
852- className = { isExpandableTool ? 'cursor-pointer' : '' }
853- onClick = { ( ) => {
854- if ( isExpandableTool ) setExpanded ( ( e ) => ! e )
855- } }
856- >
945+ < div className = { isToolNameClickable ? 'cursor-pointer' : '' } onClick = { handleToolNameClick } >
857946 < ShimmerOverlayText
858947 text = { displayName }
859948 active = { isLoadingState }
@@ -862,6 +951,21 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
862951 />
863952 </ div >
864953 { isExpandableTool && expanded && < div > { renderPendingDetails ( ) } </ div > }
954+ { showRemoveAutoAllow && isAutoAllowed && (
955+ < div className = 'mt-[8px]' >
956+ < Button
957+ onClick = { async ( ) => {
958+ await removeAutoAllowedTool ( toolCall . name )
959+ setShowRemoveAutoAllow ( false )
960+ forceUpdate ( { } )
961+ } }
962+ variant = 'default'
963+ className = 'text-xs'
964+ >
965+ Remove from Always Allowed
966+ </ Button >
967+ </ div >
968+ ) }
865969 { showButtons ? (
866970 < RunSkipButtons
867971 toolCall = { toolCall }
0 commit comments