Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/sim/executor/handlers/agent/agent-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,18 +347,18 @@ export class AgentBlockHandler implements BlockHandler {
): Promise<{ schema: any; code: string; title: string } | null> {
if (typeof window !== 'undefined') {
try {
const { useCustomToolsStore } = await import('@/stores/custom-tools')
const tool = useCustomToolsStore.getState().getTool(customToolId)
const { getCustomTool } = await import('@/hooks/queries/custom-tools')
const tool = getCustomTool(customToolId, ctx.workspaceId)
if (tool) {
return {
schema: tool.schema,
code: tool.code || '',
title: tool.title,
}
}
logger.warn(`Custom tool not found in store: ${customToolId}`)
logger.warn(`Custom tool not found in cache: ${customToolId}`)
} catch (error) {
logger.error('Error accessing custom tools store:', { error })
logger.error('Error accessing custom tools cache:', { error })
}
}

Expand Down
69 changes: 58 additions & 11 deletions apps/sim/hooks/queries/custom-tools.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
import { createLogger } from '@sim/logger'
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import type { CustomToolDefinition, CustomToolSchema } from '@/stores/custom-tools'
import { useCustomToolsStore } from '@/stores/custom-tools'
import { getQueryClient } from '@/app/_shell/providers/query-provider'

const logger = createLogger('CustomToolsQueries')
const API_ENDPOINT = '/api/tools/custom'

export interface CustomToolSchema {
type: string
function: {
name: string
description?: string
parameters: {
type: string
properties: Record<string, unknown>
required?: string[]
}
}
}

export interface CustomToolDefinition {
id: string
workspaceId: string | null
userId: string | null
title: string
schema: CustomToolSchema
code: string
createdAt: string
updatedAt?: string
}

/**
* Query key factories for custom tools queries
*/
Expand Down Expand Up @@ -64,8 +87,38 @@ function normalizeCustomTool(tool: ApiCustomTool, workspaceId: string): CustomTo
}
}

function syncCustomToolsToStore(tools: CustomToolDefinition[]) {
useCustomToolsStore.getState().setTools(tools)
/**
* Extract workspaceId from the current URL path
* Expected format: /workspace/{workspaceId}/...
*/
function getWorkspaceIdFromUrl(): string | null {
if (typeof window === 'undefined') return null
const match = window.location.pathname.match(/^\/workspace\/([^/]+)/)
return match?.[1] ?? null
}

/**
* Get all custom tools from the query cache (for non-React code)
* If workspaceId is not provided, extracts it from the current URL
*/
export function getCustomTools(workspaceId?: string): CustomToolDefinition[] {
if (typeof window === 'undefined') return []
const wsId = workspaceId ?? getWorkspaceIdFromUrl()
if (!wsId) return []
const queryClient = getQueryClient()
return queryClient.getQueryData<CustomToolDefinition[]>(customToolsKeys.list(wsId)) ?? []
}

/**
* Get a specific custom tool from the query cache by ID (for non-React code)
* If workspaceId is not provided, extracts it from the current URL
*/
export function getCustomTool(
toolId: string,
workspaceId?: string
): CustomToolDefinition | undefined {
const tools = getCustomTools(workspaceId)
return tools.find((tool) => tool.id === toolId) || tools.find((tool) => tool.title === toolId)
}

/**
Expand Down Expand Up @@ -134,19 +187,13 @@ async function fetchCustomTools(workspaceId: string): Promise<CustomToolDefiniti
* Hook to fetch custom tools
*/
export function useCustomTools(workspaceId: string) {
const query = useQuery<CustomToolDefinition[]>({
return useQuery<CustomToolDefinition[]>({
queryKey: customToolsKeys.list(workspaceId),
queryFn: () => fetchCustomTools(workspaceId),
enabled: !!workspaceId,
staleTime: 60 * 1000, // 1 minute - tools don't change frequently
placeholderData: keepPreviousData,
})

if (query.data) {
syncCustomToolsToStore(query.data)
}

return query
}

/**
Expand Down
32 changes: 0 additions & 32 deletions apps/sim/hooks/use-collaborative-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,6 @@ export function useCollaborativeWorkflow() {
const {
isConnected,
currentWorkflowId,
presenceUsers,
joinWorkflow,
leaveWorkflow,
emitWorkflowOperation,
emitSubblockUpdate,
emitVariableUpdate,
Expand Down Expand Up @@ -143,13 +140,7 @@ export function useCollaborativeWorkflow() {
// Track if we're applying remote changes to avoid infinite loops
const isApplyingRemoteChange = useRef(false)

// Track last applied position timestamps to prevent out-of-order updates
const lastPositionTimestamps = useRef<Map<string, number>>(new Map())

// Operation queue
const {
queue,
hasOperationError,
addToQueue,
confirmOperation,
failOperation,
Expand All @@ -161,22 +152,6 @@ export function useCollaborativeWorkflow() {
return !!currentWorkflowId && activeWorkflowId === currentWorkflowId
}, [currentWorkflowId, activeWorkflowId])

// Clear position timestamps when switching workflows
// Note: Workflow joining is now handled automatically by socket connect event based on URL
useEffect(() => {
if (activeWorkflowId && currentWorkflowId !== activeWorkflowId) {
logger.info(`Active workflow changed to: ${activeWorkflowId}`, {
isConnected,
currentWorkflowId,
activeWorkflowId,
presenceUsers: presenceUsers.length,
})

// Clear position timestamps when switching workflows
lastPositionTimestamps.current.clear()
}
}, [activeWorkflowId, isConnected, currentWorkflowId])

// Register emit functions with operation queue store
useEffect(() => {
registerEmitFunctions(
Expand Down Expand Up @@ -1621,15 +1596,8 @@ export function useCollaborativeWorkflow() {
)

return {
// Connection status
isConnected,
currentWorkflowId,
presenceUsers,
hasOperationError,

// Workflow management
joinWorkflow,
leaveWorkflow,

// Collaborative operations
collaborativeBatchUpdatePositions,
Expand Down
18 changes: 3 additions & 15 deletions apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
type BaseClientToolMetadata,
ClientToolCallState,
} from '@/lib/copilot/tools/client/base-tool'
import { useCustomToolsStore } from '@/stores/custom-tools'
import { getCustomTool } from '@/hooks/queries/custom-tools'
import { useCopilotStore } from '@/stores/panel/copilot/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'

Expand Down Expand Up @@ -83,17 +83,15 @@ export class ManageCustomToolClientTool extends BaseClientTool {
getDynamicText: (params, state) => {
const operation = params?.operation as 'add' | 'edit' | 'delete' | 'list' | undefined

// Return undefined if no operation yet - use static defaults
if (!operation) return undefined

// Get tool name from schema, or look it up from the store by toolId
let toolName = params?.schema?.function?.name
if (!toolName && params?.toolId) {
try {
const tool = useCustomToolsStore.getState().getTool(params.toolId)
const tool = getCustomTool(params.toolId)
toolName = tool?.schema?.function?.name
} catch {
// Ignore errors accessing store
// Ignore errors accessing cache
}
}

Expand Down Expand Up @@ -168,7 +166,6 @@ export class ManageCustomToolClientTool extends BaseClientTool {
* Add operations execute directly without confirmation.
*/
getInterruptDisplays(): BaseClientToolMetadata['interrupt'] | undefined {
// Try currentArgs first, then fall back to store (for when called before execute())
const args = this.currentArgs || this.getArgsFromStore()
const operation = args?.operation
if (operation === 'edit' || operation === 'delete') {
Expand Down Expand Up @@ -199,12 +196,9 @@ export class ManageCustomToolClientTool extends BaseClientTool {

async execute(args?: ManageCustomToolArgs): Promise<void> {
this.currentArgs = args
// For add and list operations, execute directly without confirmation
// For edit/delete, the copilot store will check hasInterrupt() and wait for confirmation
if (args?.operation === 'add' || args?.operation === 'list') {
await this.handleAccept(args)
}
// edit/delete will wait for user confirmation via handleAccept
}

/**
Expand All @@ -222,7 +216,6 @@ export class ManageCustomToolClientTool extends BaseClientTool {

const { operation, toolId, schema, code } = args

// Get workspace ID from the workflow registry
const { hydration } = useWorkflowRegistry.getState()
const workspaceId = hydration.workspaceId
if (!workspaceId) {
Expand All @@ -247,7 +240,6 @@ export class ManageCustomToolClientTool extends BaseClientTool {
await this.deleteCustomTool({ toolId, workspaceId }, logger)
break
case 'list':
// List operation is read-only, just mark as complete
await this.markToolComplete(200, 'Listed custom tools')
break
default:
Expand Down Expand Up @@ -326,13 +318,10 @@ export class ManageCustomToolClientTool extends BaseClientTool {
throw new Error('Tool ID is required for editing a custom tool')
}

// At least one of schema or code must be provided
if (!schema && !code) {
throw new Error('At least one of schema or code must be provided for editing')
}

// We need to send the full tool data to the API for updates
// First, fetch the existing tool to merge with updates
const existingResponse = await fetch(`${API_ENDPOINT}?workspaceId=${workspaceId}`)
const existingData = await existingResponse.json()

Expand All @@ -345,7 +334,6 @@ export class ManageCustomToolClientTool extends BaseClientTool {
throw new Error(`Tool with ID ${toolId} not found`)
}

// Merge updates with existing tool - use function name as title
const mergedSchema = schema ?? existingTool.schema
const updatedTool = {
id: toolId,
Expand Down
47 changes: 0 additions & 47 deletions apps/sim/providers/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
getAllProviderIds,
getApiKey,
getBaseModelProviders,
getCustomTools,
getHostedModels,
getMaxTemperature,
getProvider,
Expand All @@ -30,7 +29,6 @@ import {
shouldBillModelUsage,
supportsTemperature,
supportsToolUsageControl,
transformCustomTool,
updateOllamaProviderModels,
} from '@/providers/utils'

Expand Down Expand Up @@ -837,51 +835,6 @@ describe('JSON and Structured Output', () => {
})

describe('Tool Management', () => {
describe('transformCustomTool', () => {
it.concurrent('should transform valid custom tool schema', () => {
const customTool = {
id: 'test-tool',
schema: {
function: {
name: 'testFunction',
description: 'A test function',
parameters: {
type: 'object',
properties: {
input: { type: 'string', description: 'Input parameter' },
},
required: ['input'],
},
},
},
}

const result = transformCustomTool(customTool)

expect(result.id).toBe('custom_test-tool')
expect(result.name).toBe('testFunction')
expect(result.description).toBe('A test function')
expect(result.parameters.type).toBe('object')
expect(result.parameters.properties).toBeDefined()
expect(result.parameters.required).toEqual(['input'])
})

it.concurrent('should throw error for invalid schema', () => {
const invalidTool = { id: 'test', schema: null }
expect(() => transformCustomTool(invalidTool)).toThrow('Invalid custom tool schema')

const noFunction = { id: 'test', schema: {} }
expect(() => transformCustomTool(noFunction)).toThrow('Invalid custom tool schema')
})
})

describe('getCustomTools', () => {
it.concurrent('should return array of transformed custom tools', () => {
const result = getCustomTools()
expect(Array.isArray(result)).toBe(true)
})
})

describe('prepareToolsWithUsageControl', () => {
const mockLogger = {
info: vi.fn(),
Expand Down
33 changes: 0 additions & 33 deletions apps/sim/providers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
updateOllamaModels as updateOllamaModelsInDefinitions,
} from '@/providers/models'
import type { ProviderId, ProviderToolConfig } from '@/providers/types'
import { useCustomToolsStore } from '@/stores/custom-tools/store'
import { useProvidersStore } from '@/stores/providers/store'
import { mergeToolParameters } from '@/tools/params'

Expand Down Expand Up @@ -419,38 +418,6 @@ export function extractAndParseJSON(content: string): any {
}
}

/**
* Transforms a custom tool schema into a provider tool config
*/
export function transformCustomTool(customTool: any): ProviderToolConfig {
const schema = customTool.schema

if (!schema || !schema.function) {
throw new Error('Invalid custom tool schema')
}

return {
id: `custom_${customTool.id}`,
name: schema.function.name,
description: schema.function.description || '',
params: {},
parameters: {
type: schema.function.parameters.type,
properties: schema.function.parameters.properties,
required: schema.function.parameters.required || [],
},
}
}

/**
* Gets all available custom tools as provider tool configs
*/
export function getCustomTools(): ProviderToolConfig[] {
const customTools = useCustomToolsStore.getState().getAllTools()

return customTools.map(transformCustomTool)
}

/**
* Transforms a block tool into a provider tool config with operation selection
*
Expand Down
8 changes: 0 additions & 8 deletions apps/sim/stores/custom-tools/index.ts

This file was deleted.

Loading