@@ -293,11 +293,33 @@ async function handleRun(
293293
294294 // Handle integration tools (server-side execution)
295295 if ( ! instance && isIntegrationTool ( toolCall . name ) ) {
296+ // Set executing state immediately for UI feedback
297+ setToolCallState ( toolCall , 'executing' )
298+ onStateChange ?.( 'executing' )
296299 try {
297- onStateChange ?.( 'executing' )
298300 await useCopilotStore . getState ( ) . executeIntegrationTool ( toolCall . id )
301+ // Note: executeIntegrationTool handles success/error state updates internally
299302 } catch ( e ) {
300- setToolCallState ( toolCall , 'errored' , { error : e instanceof Error ? e . message : String ( e ) } )
303+ // If executeIntegrationTool throws, ensure we update state to error
304+ setToolCallState ( toolCall , 'error' , { error : e instanceof Error ? e . message : String ( e ) } )
305+ onStateChange ?.( 'error' )
306+ // Notify backend about the error so agent doesn't hang
307+ try {
308+ await fetch ( '/api/copilot/tools/mark-complete' , {
309+ method : 'POST' ,
310+ headers : { 'Content-Type' : 'application/json' } ,
311+ body : JSON . stringify ( {
312+ id : toolCall . id ,
313+ name : toolCall . name ,
314+ status : 500 ,
315+ message : e instanceof Error ? e . message : 'Tool execution failed' ,
316+ data : { error : e instanceof Error ? e . message : String ( e ) } ,
317+ } ) ,
318+ } )
319+ } catch {
320+ // Last resort: log error if we can't notify backend
321+ console . error ( '[handleRun] Failed to notify backend of tool error:' , toolCall . id )
322+ }
301323 }
302324 return
303325 }
@@ -313,7 +335,7 @@ async function handleRun(
313335 await instance . handleAccept ?.( mergedParams )
314336 onStateChange ?.( 'executing' )
315337 } catch ( e ) {
316- setToolCallState ( toolCall , 'errored ' , { error : e instanceof Error ? e . message : String ( e ) } )
338+ setToolCallState ( toolCall , 'error ' , { error : e instanceof Error ? e . message : String ( e ) } )
317339 }
318340}
319341
@@ -324,19 +346,37 @@ async function handleSkip(toolCall: CopilotToolCall, setToolCallState: any, onSt
324346 if ( ! instance && isIntegrationTool ( toolCall . name ) ) {
325347 setToolCallState ( toolCall , 'rejected' )
326348 onStateChange ?.( 'rejected' )
327- // Notify backend that tool was skipped
328- try {
329- await fetch ( '/api/copilot/tools/mark-complete' , {
330- method : 'POST' ,
331- headers : { 'Content-Type' : 'application/json' } ,
332- body : JSON . stringify ( {
333- id : toolCall . id ,
334- name : toolCall . name ,
335- status : 400 ,
336- message : 'Tool execution skipped by user' ,
337- } ) ,
338- } )
339- } catch { }
349+
350+ // Notify backend that tool was skipped - this is CRITICAL for the agent to continue
351+ // Retry up to 3 times if the notification fails
352+ let notified = false
353+ for ( let attempt = 0 ; attempt < 3 && ! notified ; attempt ++ ) {
354+ try {
355+ const res = await fetch ( '/api/copilot/tools/mark-complete' , {
356+ method : 'POST' ,
357+ headers : { 'Content-Type' : 'application/json' } ,
358+ body : JSON . stringify ( {
359+ id : toolCall . id ,
360+ name : toolCall . name ,
361+ status : 400 ,
362+ message : 'Tool execution skipped by user' ,
363+ data : { skipped : true , reason : 'user_skipped' } ,
364+ } ) ,
365+ } )
366+ if ( res . ok ) {
367+ notified = true
368+ }
369+ } catch ( e ) {
370+ // Wait briefly before retry
371+ if ( attempt < 2 ) {
372+ await new Promise ( ( resolve ) => setTimeout ( resolve , 500 ) )
373+ }
374+ }
375+ }
376+
377+ if ( ! notified ) {
378+ console . error ( '[handleSkip] Failed to notify backend after 3 attempts:' , toolCall . id )
379+ }
340380 return
341381 }
342382
@@ -408,6 +448,7 @@ function RunSkipButtons({
408448} ) {
409449 const [ isProcessing , setIsProcessing ] = useState ( false )
410450 const [ buttonsHidden , setButtonsHidden ] = useState ( false )
451+ const actionInProgressRef = useRef ( false )
411452 const { setToolCallState, addAutoAllowedTool } = useCopilotStore ( )
412453
413454 const instance = getClientTool ( toolCall . id )
@@ -420,25 +461,47 @@ function RunSkipButtons({
420461 const rejectLabel = interruptDisplays ?. reject ?. text || 'Skip'
421462
422463 const onRun = async ( ) => {
464+ // Prevent race condition - check ref synchronously
465+ if ( actionInProgressRef . current ) return
466+ actionInProgressRef . current = true
423467 setIsProcessing ( true )
424468 setButtonsHidden ( true )
425469 try {
426470 await handleRun ( toolCall , setToolCallState , onStateChange , editedParams )
427471 } finally {
428472 setIsProcessing ( false )
473+ actionInProgressRef . current = false
429474 }
430475 }
431476
432477 const onAlwaysAllow = async ( ) => {
478+ // Prevent race condition - check ref synchronously
479+ if ( actionInProgressRef . current ) return
480+ actionInProgressRef . current = true
433481 setIsProcessing ( true )
434482 setButtonsHidden ( true )
435483 try {
436- // Add to auto-allowed list
484+ // Add to auto-allowed list first
437485 await addAutoAllowedTool ( toolCall . name )
438486 // Then execute
439487 await handleRun ( toolCall , setToolCallState , onStateChange , editedParams )
440488 } finally {
441489 setIsProcessing ( false )
490+ actionInProgressRef . current = false
491+ }
492+ }
493+
494+ const onSkip = async ( ) => {
495+ // Prevent race condition - check ref synchronously
496+ if ( actionInProgressRef . current ) return
497+ actionInProgressRef . current = true
498+ setIsProcessing ( true )
499+ setButtonsHidden ( true )
500+ try {
501+ await handleSkip ( toolCall , setToolCallState , onStateChange )
502+ } finally {
503+ setIsProcessing ( false )
504+ actionInProgressRef . current = false
442505 }
443506 }
444507
@@ -456,14 +519,7 @@ function RunSkipButtons({
456519 Always Allow
457520 </ Button >
458521 ) }
459- < Button
460- onClick = { async ( ) => {
461- setButtonsHidden ( true )
462- await handleSkip ( toolCall , setToolCallState , onStateChange )
463- } }
464- disabled = { isProcessing }
465- variant = 'default'
466- >
522+ < Button onClick = { onSkip } disabled = { isProcessing } variant = 'default' >
467523 { rejectLabel }
468524 </ Button >
469525 </ div >
@@ -495,8 +551,10 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
495551 const paramsRef = useRef ( params )
496552
497553 // Check if this integration tool is auto-allowed
498- const { isToolAutoAllowed, removeAutoAllowedTool } = useCopilotStore ( )
499- const isAutoAllowed = isIntegrationTool ( toolCall . name ) && isToolAutoAllowed ( toolCall . name )
554+ // Subscribe to autoAllowedTools so we re-render when it changes
555+ const autoAllowedTools = useCopilotStore ( ( s ) => s . autoAllowedTools )
556+ const { removeAutoAllowedTool } = useCopilotStore ( )
557+ const isAutoAllowed = isIntegrationTool ( toolCall . name ) && autoAllowedTools . includes ( toolCall . name )
500558
501559 // Update edited params when toolCall params change (deep comparison to avoid resetting user edits on ref change)
502560 useEffect ( ( ) => {
@@ -888,15 +946,40 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
888946
889947 // Special handling for set_environment_variables - always stacked, always expanded
890948 if ( toolCall . name === 'set_environment_variables' && toolCall . state === 'pending' ) {
949+ const isEnvVarsClickable = isAutoAllowed
950+
951+ const handleEnvVarsClick = ( ) => {
952+ if ( isAutoAllowed ) {
953+ setShowRemoveAutoAllow ( ( prev ) => ! prev )
954+ }
955+ }
956+
891957 return (
892958 < div className = 'w-full' >
893- < ShimmerOverlayText
894- text = { displayName }
895- active = { isLoadingState }
896- isSpecial = { isSpecial }
897- className = 'font-[470] font-season text-[#3a3d41] text-sm dark:text-[#939393]'
898- />
959+ < div className = { isEnvVarsClickable ? 'cursor-pointer' : '' } onClick = { handleEnvVarsClick } >
960+ < ShimmerOverlayText
961+ text = { displayName }
962+ active = { isLoadingState }
963+ isSpecial = { isSpecial }
964+ className = 'font-[470] font-season text-[#3a3d41] text-sm dark:text-[#939393]'
965+ />
966+ </ div >
899967 < div className = 'mt-[8px]' > { renderPendingDetails ( ) } </ div >
968+ { showRemoveAutoAllow && isAutoAllowed && (
969+ < div className = 'mt-[8px]' >
970+ < Button
971+ onClick = { async ( ) => {
972+ await removeAutoAllowedTool ( toolCall . name )
973+ setShowRemoveAutoAllow ( false )
974+ forceUpdate ( { } )
975+ } }
976+ variant = 'default'
977+ className = 'text-xs'
978+ >
979+ Remove from Always Allowed
980+ </ Button >
981+ </ div >
982+ ) }
900983 { showButtons && (
901984 < RunSkipButtons
902985 toolCall = { toolCall }
@@ -911,20 +994,47 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
911994 // Special rendering for function_execute - show code block
912995 if ( toolCall . name === 'function_execute' ) {
913996 const code = params . code || ''
997+ const isFunctionExecuteClickable = isAutoAllowed
998+
999+ const handleFunctionExecuteClick = ( ) => {
1000+ if ( isAutoAllowed ) {
1001+ setShowRemoveAutoAllow ( ( prev ) => ! prev )
1002+ }
1003+ }
9141004
9151005 return (
9161006 < div className = 'w-full' >
917- < ShimmerOverlayText
918- text = { displayName }
919- active = { isLoadingState }
920- isSpecial = { false }
921- className = 'font-[470] font-season text-[#939393] text-sm dark:text-[#939393]'
922- />
1007+ < div
1008+ className = { isFunctionExecuteClickable ? 'cursor-pointer' : '' }
1009+ onClick = { handleFunctionExecuteClick }
1010+ >
1011+ < ShimmerOverlayText
1012+ text = { displayName }
1013+ active = { isLoadingState }
1014+ isSpecial = { false }
1015+ className = 'font-[470] font-season text-[#939393] text-sm dark:text-[#939393]'
1016+ />
1017+ </ div >
9231018 { code && (
9241019 < div className = 'mt-2' >
9251020 < Code . Viewer code = { code } language = 'javascript' showGutter />
9261021 </ div >
9271022 ) }
1023+ { showRemoveAutoAllow && isAutoAllowed && (
1024+ < div className = 'mt-[8px]' >
1025+ < Button
1026+ onClick = { async ( ) => {
1027+ await removeAutoAllowedTool ( toolCall . name )
1028+ setShowRemoveAutoAllow ( false )
1029+ forceUpdate ( { } )
1030+ } }
1031+ variant = 'default'
1032+ className = 'text-xs'
1033+ >
1034+ Remove from Always Allowed
1035+ </ Button >
1036+ </ div >
1037+ ) }
9281038 { showButtons && (
9291039 < RunSkipButtons
9301040 toolCall = { toolCall }
0 commit comments