Skip to content

Commit 6b4d762

Browse files
authored
fix(custom-tools, copilot): custom tools state + copilot fixes (#2264)
* Workspace env vars * Fix execution animation on copilot run * Custom tools toolg * Custom tools * Fix custom tool * remove extra fallback * Fix lint
1 parent b7a1e8f commit 6b4d762

File tree

25 files changed

+1452
-550
lines changed

25 files changed

+1452
-550
lines changed

apps/sim/app/api/copilot/chat/route.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -353,10 +353,10 @@ export async function POST(req: NextRequest) {
353353
executeLocally: true,
354354
},
355355
]
356-
// Fetch user credentials (OAuth + API keys)
356+
// Fetch user credentials (OAuth + API keys) - pass workflowId to get workspace env vars
357357
try {
358358
const rawCredentials = await getCredentialsServerTool.execute(
359-
{},
359+
{ workflowId },
360360
{ userId: authenticatedUserId }
361361
)
362362

@@ -840,9 +840,36 @@ export async function POST(req: NextRequest) {
840840
}
841841
} catch (error) {
842842
logger.error(`[${tracker.requestId}] Error processing stream:`, error)
843-
controller.error(error)
843+
844+
// Send an error event to the client before closing so it knows what happened
845+
try {
846+
const errorMessage =
847+
error instanceof Error && error.message === 'terminated'
848+
? 'Connection to AI service was interrupted. Please try again.'
849+
: 'An unexpected error occurred while processing the response.'
850+
const encoder = new TextEncoder()
851+
852+
// Send error as content so it shows in the chat
853+
controller.enqueue(
854+
encoder.encode(
855+
`data: ${JSON.stringify({ type: 'content', data: `\n\n_${errorMessage}_` })}\n\n`
856+
)
857+
)
858+
// Send done event to properly close the stream on client
859+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'done' })}\n\n`))
860+
} catch (enqueueError) {
861+
// Stream might already be closed, that's ok
862+
logger.warn(
863+
`[${tracker.requestId}] Could not send error event to client:`,
864+
enqueueError
865+
)
866+
}
844867
} finally {
845-
controller.close()
868+
try {
869+
controller.close()
870+
} catch {
871+
// Controller might already be closed
872+
}
846873
}
847874
},
848875
})

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

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -451,15 +451,6 @@ function RunSkipButtons({
451451
const actionInProgressRef = useRef(false)
452452
const { setToolCallState, addAutoAllowedTool } = useCopilotStore()
453453

454-
const instance = getClientTool(toolCall.id)
455-
const interruptDisplays = instance?.getInterruptDisplays?.()
456-
const isIntegration = isIntegrationTool(toolCall.name)
457-
458-
// For integration tools: Allow, Always Allow, Skip
459-
// For client tools with interrupts: Run, Skip (or custom labels)
460-
const acceptLabel = isIntegration ? 'Allow' : interruptDisplays?.accept?.text || 'Run'
461-
const rejectLabel = interruptDisplays?.reject?.text || 'Skip'
462-
463454
const onRun = async () => {
464455
// Prevent race condition - check ref synchronously
465456
if (actionInProgressRef.current) return
@@ -507,20 +498,19 @@ function RunSkipButtons({
507498

508499
if (buttonsHidden) return null
509500

501+
// Standardized buttons for all interrupt tools: Allow, Always Allow, Skip
510502
return (
511503
<div className='mt-[12px] flex gap-[6px]'>
512504
<Button onClick={onRun} disabled={isProcessing} variant='primary'>
513505
{isProcessing ? <Loader2 className='mr-1 h-3 w-3 animate-spin' /> : null}
514-
{acceptLabel}
506+
Allow
507+
</Button>
508+
<Button onClick={onAlwaysAllow} disabled={isProcessing} variant='default'>
509+
{isProcessing ? <Loader2 className='mr-1 h-3 w-3 animate-spin' /> : null}
510+
Always Allow
515511
</Button>
516-
{isIntegration && (
517-
<Button onClick={onAlwaysAllow} disabled={isProcessing} variant='default'>
518-
{isProcessing ? <Loader2 className='mr-1 h-3 w-3 animate-spin' /> : null}
519-
Always Allow
520-
</Button>
521-
)}
522512
<Button onClick={onSkip} disabled={isProcessing} variant='default'>
523-
{rejectLabel}
513+
Skip
524514
</Button>
525515
</div>
526516
)

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,15 +190,25 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
190190

191191
/**
192192
* Cleanup on component unmount (page refresh, navigation, etc.)
193+
* Uses a ref to track sending state to avoid stale closure issues
194+
* Note: Parent workflow.tsx also has useStreamCleanup for page-level cleanup
193195
*/
196+
const isSendingRef = useRef(isSendingMessage)
197+
isSendingRef.current = isSendingMessage
198+
const abortMessageRef = useRef(abortMessage)
199+
abortMessageRef.current = abortMessage
200+
194201
useEffect(() => {
195202
return () => {
196-
if (isSendingMessage) {
197-
abortMessage()
203+
// Use refs to check current values, not stale closure values
204+
if (isSendingRef.current) {
205+
abortMessageRef.current()
198206
logger.info('Aborted active message streaming due to component unmount')
199207
}
200208
}
201-
}, [isSendingMessage, abortMessage])
209+
// Empty deps - only run cleanup on actual unmount, not on re-renders
210+
// eslint-disable-next-line react-hooks/exhaustive-deps
211+
}, [])
202212

203213
/**
204214
* Container-level click capture to cancel edit mode when clicking outside the current edit area

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ interface CustomToolModalProps {
5858

5959
export interface CustomTool {
6060
type: 'custom-tool'
61+
id?: string
6162
title: string
6263
name: string
6364
description: string
@@ -433,6 +434,8 @@ try {
433434
}
434435
}
435436

437+
let savedToolId: string | undefined
438+
436439
if (isEditing && toolIdToUpdate) {
437440
await updateToolMutation.mutateAsync({
438441
workspaceId,
@@ -443,19 +446,23 @@ try {
443446
code: functionCode || '',
444447
},
445448
})
449+
savedToolId = toolIdToUpdate
446450
} else {
447-
await createToolMutation.mutateAsync({
451+
const result = await createToolMutation.mutateAsync({
448452
workspaceId,
449453
tool: {
450454
title: name,
451455
schema,
452456
code: functionCode || '',
453457
},
454458
})
459+
// Get the ID from the created tool
460+
savedToolId = result?.[0]?.id
455461
}
456462

457463
const customTool: CustomTool = {
458464
type: 'custom-tool',
465+
id: savedToolId,
459466
title: name,
460467
name,
461468
description,

0 commit comments

Comments
 (0)