Skip to content

Commit 656dfaf

Browse files
authored
fix(copilot): fix function execute tool (#2222)
1 parent 9f604f3 commit 656dfaf

File tree

1 file changed

+149
-39
lines changed
  • apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call

1 file changed

+149
-39
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx

Lines changed: 149 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)