-
{
- if (isExpandableTool) setExpanded((e) => !e)
- }}
- >
+
{isExpandableTool && expanded &&
{renderPendingDetails()}
}
+ {showRemoveAutoAllow && isAutoAllowed && (
+
+
+
+ )}
{showButtons ? (
Date: Fri, 5 Dec 2025 15:12:35 -0800
Subject: [PATCH 2/3] fix(copilot): fix tool call flash (#2221)
* Fix copilot tool call flash
* Fix lint
---
.../copilot/components/tool-call/tool-call.tsx | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx
index 05c642bacf..75bfbc3743 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx
@@ -268,8 +268,15 @@ function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean {
}
// Also show buttons for integration tools in pending state (they need user confirmation)
+ // But NOT if the tool is auto-allowed (it will auto-execute)
const mode = useCopilotStore.getState().mode
- if (mode === 'build' && isIntegrationTool(toolCall.name) && toolCall.state === 'pending') {
+ const isAutoAllowed = useCopilotStore.getState().isToolAutoAllowed(toolCall.name)
+ if (
+ mode === 'build' &&
+ isIntegrationTool(toolCall.name) &&
+ toolCall.state === 'pending' &&
+ !isAutoAllowed
+ ) {
return true
}
From 656dfafb8fa03a1004059ff12304f9fff8a411c0 Mon Sep 17 00:00:00 2001
From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Date: Fri, 5 Dec 2025 15:19:39 -0800
Subject: [PATCH 3/3] fix(copilot): fix function execute tool (#2222)
---
.../components/tool-call/tool-call.tsx | 188 ++++++++++++++----
1 file changed, 149 insertions(+), 39 deletions(-)
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx
index 75bfbc3743..86cb7e639a 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx
@@ -293,11 +293,33 @@ async function handleRun(
// Handle integration tools (server-side execution)
if (!instance && isIntegrationTool(toolCall.name)) {
+ // Set executing state immediately for UI feedback
+ setToolCallState(toolCall, 'executing')
+ onStateChange?.('executing')
try {
- onStateChange?.('executing')
await useCopilotStore.getState().executeIntegrationTool(toolCall.id)
+ // Note: executeIntegrationTool handles success/error state updates internally
} catch (e) {
- setToolCallState(toolCall, 'errored', { error: e instanceof Error ? e.message : String(e) })
+ // If executeIntegrationTool throws, ensure we update state to error
+ setToolCallState(toolCall, 'error', { error: e instanceof Error ? e.message : String(e) })
+ onStateChange?.('error')
+ // Notify backend about the error so agent doesn't hang
+ try {
+ await fetch('/api/copilot/tools/mark-complete', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ id: toolCall.id,
+ name: toolCall.name,
+ status: 500,
+ message: e instanceof Error ? e.message : 'Tool execution failed',
+ data: { error: e instanceof Error ? e.message : String(e) },
+ }),
+ })
+ } catch {
+ // Last resort: log error if we can't notify backend
+ console.error('[handleRun] Failed to notify backend of tool error:', toolCall.id)
+ }
}
return
}
@@ -313,7 +335,7 @@ async function handleRun(
await instance.handleAccept?.(mergedParams)
onStateChange?.('executing')
} catch (e) {
- setToolCallState(toolCall, 'errored', { error: e instanceof Error ? e.message : String(e) })
+ setToolCallState(toolCall, 'error', { error: e instanceof Error ? e.message : String(e) })
}
}
@@ -324,19 +346,37 @@ async function handleSkip(toolCall: CopilotToolCall, setToolCallState: any, onSt
if (!instance && isIntegrationTool(toolCall.name)) {
setToolCallState(toolCall, 'rejected')
onStateChange?.('rejected')
- // Notify backend that tool was skipped
- try {
- await fetch('/api/copilot/tools/mark-complete', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- id: toolCall.id,
- name: toolCall.name,
- status: 400,
- message: 'Tool execution skipped by user',
- }),
- })
- } catch {}
+
+ // Notify backend that tool was skipped - this is CRITICAL for the agent to continue
+ // Retry up to 3 times if the notification fails
+ let notified = false
+ for (let attempt = 0; attempt < 3 && !notified; attempt++) {
+ try {
+ const res = await fetch('/api/copilot/tools/mark-complete', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ id: toolCall.id,
+ name: toolCall.name,
+ status: 400,
+ message: 'Tool execution skipped by user',
+ data: { skipped: true, reason: 'user_skipped' },
+ }),
+ })
+ if (res.ok) {
+ notified = true
+ }
+ } catch (e) {
+ // Wait briefly before retry
+ if (attempt < 2) {
+ await new Promise((resolve) => setTimeout(resolve, 500))
+ }
+ }
+ }
+
+ if (!notified) {
+ console.error('[handleSkip] Failed to notify backend after 3 attempts:', toolCall.id)
+ }
return
}
@@ -408,6 +448,7 @@ function RunSkipButtons({
}) {
const [isProcessing, setIsProcessing] = useState(false)
const [buttonsHidden, setButtonsHidden] = useState(false)
+ const actionInProgressRef = useRef(false)
const { setToolCallState, addAutoAllowedTool } = useCopilotStore()
const instance = getClientTool(toolCall.id)
@@ -420,25 +461,47 @@ function RunSkipButtons({
const rejectLabel = interruptDisplays?.reject?.text || 'Skip'
const onRun = async () => {
+ // Prevent race condition - check ref synchronously
+ if (actionInProgressRef.current) return
+ actionInProgressRef.current = true
setIsProcessing(true)
setButtonsHidden(true)
try {
await handleRun(toolCall, setToolCallState, onStateChange, editedParams)
} finally {
setIsProcessing(false)
+ actionInProgressRef.current = false
}
}
const onAlwaysAllow = async () => {
+ // Prevent race condition - check ref synchronously
+ if (actionInProgressRef.current) return
+ actionInProgressRef.current = true
setIsProcessing(true)
setButtonsHidden(true)
try {
- // Add to auto-allowed list
+ // Add to auto-allowed list first
await addAutoAllowedTool(toolCall.name)
// Then execute
await handleRun(toolCall, setToolCallState, onStateChange, editedParams)
} finally {
setIsProcessing(false)
+ actionInProgressRef.current = false
+ }
+ }
+
+ const onSkip = async () => {
+ // Prevent race condition - check ref synchronously
+ if (actionInProgressRef.current) return
+ actionInProgressRef.current = true
+ setIsProcessing(true)
+ setButtonsHidden(true)
+ try {
+ await handleSkip(toolCall, setToolCallState, onStateChange)
+ } finally {
+ setIsProcessing(false)
+ actionInProgressRef.current = false
}
}
@@ -456,14 +519,7 @@ function RunSkipButtons({
Always Allow
)}
-
@@ -495,8 +551,10 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
const paramsRef = useRef(params)
// Check if this integration tool is auto-allowed
- const { isToolAutoAllowed, removeAutoAllowedTool } = useCopilotStore()
- const isAutoAllowed = isIntegrationTool(toolCall.name) && isToolAutoAllowed(toolCall.name)
+ // Subscribe to autoAllowedTools so we re-render when it changes
+ const autoAllowedTools = useCopilotStore((s) => s.autoAllowedTools)
+ const { removeAutoAllowedTool } = useCopilotStore()
+ const isAutoAllowed = isIntegrationTool(toolCall.name) && autoAllowedTools.includes(toolCall.name)
// Update edited params when toolCall params change (deep comparison to avoid resetting user edits on ref change)
useEffect(() => {
@@ -888,15 +946,40 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
// Special handling for set_environment_variables - always stacked, always expanded
if (toolCall.name === 'set_environment_variables' && toolCall.state === 'pending') {
+ const isEnvVarsClickable = isAutoAllowed
+
+ const handleEnvVarsClick = () => {
+ if (isAutoAllowed) {
+ setShowRemoveAutoAllow((prev) => !prev)
+ }
+ }
+
return (
-
+
+
+
{renderPendingDetails()}
+ {showRemoveAutoAllow && isAutoAllowed && (
+
+ {
+ await removeAutoAllowedTool(toolCall.name)
+ setShowRemoveAutoAllow(false)
+ forceUpdate({})
+ }}
+ variant='default'
+ className='text-xs'
+ >
+ Remove from Always Allowed
+
+
+ )}
{showButtons && (
{
+ if (isAutoAllowed) {
+ setShowRemoveAutoAllow((prev) => !prev)
+ }
+ }
return (
-
+
+
+
{code && (
)}
+ {showRemoveAutoAllow && isAutoAllowed && (
+
+ {
+ await removeAutoAllowedTool(toolCall.name)
+ setShowRemoveAutoAllow(false)
+ forceUpdate({})
+ }}
+ variant='default'
+ className='text-xs'
+ >
+ Remove from Always Allowed
+
+
+ )}
{showButtons && (