From 75da06adf875784dd8eb8d08dcfed5d8a91e9b5a Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Date: Fri, 5 Dec 2025 14:59:38 -0800 Subject: [PATCH 1/3] fix(copilot): fix hanging tool calls (#2218) --- .../components/tool-call/tool-call.tsx | 122 ++++++++++++++++-- 1 file changed, 113 insertions(+), 9 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 dad34e321f..05c642bacf 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 @@ -237,6 +237,16 @@ function isSpecialToolCall(toolCall: CopilotToolCall): boolean { return workflowOperationTools.includes(toolCall.name) } +/** + * Checks if a tool is an integration tool (server-side executed, not a client tool) + */ +function isIntegrationTool(toolName: string): boolean { + // Check if it's NOT a client tool (not in CLASS_TOOL_METADATA and not in registered tools) + const isClientTool = !!CLASS_TOOL_METADATA[toolName] + const isRegisteredTool = !!getRegisteredTools()[toolName] + return !isClientTool && !isRegisteredTool +} + function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean { const instance = getClientTool(toolCall.id) let hasInterrupt = !!instance?.getInterruptDisplays?.() @@ -251,7 +261,19 @@ function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean { } } catch {} } - return hasInterrupt && toolCall.state === 'pending' + + // Show buttons for client tools with interrupts + if (hasInterrupt && toolCall.state === 'pending') { + return true + } + + // Also show buttons for integration tools in pending state (they need user confirmation) + const mode = useCopilotStore.getState().mode + if (mode === 'build' && isIntegrationTool(toolCall.name) && toolCall.state === 'pending') { + return true + } + + return false } async function handleRun( @@ -261,6 +283,18 @@ async function handleRun( editedParams?: any ) { const instance = getClientTool(toolCall.id) + + // Handle integration tools (server-side execution) + if (!instance && isIntegrationTool(toolCall.name)) { + try { + onStateChange?.('executing') + await useCopilotStore.getState().executeIntegrationTool(toolCall.id) + } catch (e) { + setToolCallState(toolCall, 'errored', { error: e instanceof Error ? e.message : String(e) }) + } + return + } + if (!instance) return try { const mergedParams = @@ -278,6 +312,27 @@ async function handleRun( async function handleSkip(toolCall: CopilotToolCall, setToolCallState: any, onStateChange?: any) { const instance = getClientTool(toolCall.id) + + // Handle integration tools (skip by marking as rejected and notifying backend) + 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 {} + return + } + if (instance) { try { await instance.handleReject?.() @@ -346,11 +401,15 @@ function RunSkipButtons({ }) { const [isProcessing, setIsProcessing] = useState(false) const [buttonsHidden, setButtonsHidden] = useState(false) - const { setToolCallState } = useCopilotStore() + const { setToolCallState, addAutoAllowedTool } = useCopilotStore() const instance = getClientTool(toolCall.id) const interruptDisplays = instance?.getInterruptDisplays?.() - const acceptLabel = interruptDisplays?.accept?.text || 'Run' + const isIntegration = isIntegrationTool(toolCall.name) + + // For integration tools: Allow, Always Allow, Skip + // For client tools with interrupts: Run, Skip (or custom labels) + const acceptLabel = isIntegration ? 'Allow' : interruptDisplays?.accept?.text || 'Run' const rejectLabel = interruptDisplays?.reject?.text || 'Skip' const onRun = async () => { @@ -363,6 +422,19 @@ function RunSkipButtons({ } } + const onAlwaysAllow = async () => { + setIsProcessing(true) + setButtonsHidden(true) + try { + // Add to auto-allowed list + await addAutoAllowedTool(toolCall.name) + // Then execute + await handleRun(toolCall, setToolCallState, onStateChange, editedParams) + } finally { + setIsProcessing(false) + } + } + if (buttonsHidden) return null return ( @@ -371,6 +443,12 @@ function RunSkipButtons({ {isProcessing ? : null} {acceptLabel} + {isIntegration && ( + + )} + + )} {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 && ( +
+ +
+ )} {showButtons && ( { + if (isAutoAllowed) { + setShowRemoveAutoAllow((prev) => !prev) + } + } return (
- +
+ +
{code && (
)} + {showRemoveAutoAllow && isAutoAllowed && ( +
+ +
+ )} {showButtons && (