diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 098b813af5..3240da8974 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -347,8 +347,8 @@ 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, @@ -356,9 +356,9 @@ export class AgentBlockHandler implements BlockHandler { 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 }) } } diff --git a/apps/sim/hooks/queries/custom-tools.ts b/apps/sim/hooks/queries/custom-tools.ts index b85e08d8c4..c86333e143 100644 --- a/apps/sim/hooks/queries/custom-tools.ts +++ b/apps/sim/hooks/queries/custom-tools.ts @@ -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 + 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 */ @@ -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(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) } /** @@ -134,19 +187,13 @@ async function fetchCustomTools(workspaceId: string): Promise({ + return useQuery({ 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 } /** diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index b9ff1fa873..1b39b7676d 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -113,9 +113,6 @@ export function useCollaborativeWorkflow() { const { isConnected, currentWorkflowId, - presenceUsers, - joinWorkflow, - leaveWorkflow, emitWorkflowOperation, emitSubblockUpdate, emitVariableUpdate, @@ -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>(new Map()) - - // Operation queue const { - queue, - hasOperationError, addToQueue, confirmOperation, failOperation, @@ -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( @@ -1621,15 +1596,8 @@ export function useCollaborativeWorkflow() { ) return { - // Connection status isConnected, currentWorkflowId, - presenceUsers, - hasOperationError, - - // Workflow management - joinWorkflow, - leaveWorkflow, // Collaborative operations collaborativeBatchUpdatePositions, diff --git a/apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts b/apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts index 202864e169..58a8236376 100644 --- a/apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts +++ b/apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts @@ -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' @@ -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 } } @@ -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') { @@ -199,12 +196,9 @@ export class ManageCustomToolClientTool extends BaseClientTool { async execute(args?: ManageCustomToolArgs): Promise { 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 } /** @@ -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) { @@ -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: @@ -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() @@ -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, diff --git a/apps/sim/providers/utils.test.ts b/apps/sim/providers/utils.test.ts index db6dcfd329..f2181c392b 100644 --- a/apps/sim/providers/utils.test.ts +++ b/apps/sim/providers/utils.test.ts @@ -11,7 +11,6 @@ import { getAllProviderIds, getApiKey, getBaseModelProviders, - getCustomTools, getHostedModels, getMaxTemperature, getProvider, @@ -30,7 +29,6 @@ import { shouldBillModelUsage, supportsTemperature, supportsToolUsageControl, - transformCustomTool, updateOllamaProviderModels, } from '@/providers/utils' @@ -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(), diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index 9d42bee2a1..def15fbb51 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -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' @@ -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 * diff --git a/apps/sim/stores/custom-tools/index.ts b/apps/sim/stores/custom-tools/index.ts deleted file mode 100644 index 808dbd8f05..0000000000 --- a/apps/sim/stores/custom-tools/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { useCustomToolsStore } from './store' -export type { - CustomToolDefinition, - CustomToolSchema, - CustomToolsActions, - CustomToolsState, - CustomToolsStore, -} from './types' diff --git a/apps/sim/stores/custom-tools/store.ts b/apps/sim/stores/custom-tools/store.ts deleted file mode 100644 index bc0ccc2064..0000000000 --- a/apps/sim/stores/custom-tools/store.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createLogger } from '@sim/logger' -import { create } from 'zustand' -import { devtools } from 'zustand/middleware' -import type { CustomToolsState, CustomToolsStore } from './types' - -const logger = createLogger('CustomToolsStore') - -const initialState: CustomToolsState = { - tools: [], -} - -export const useCustomToolsStore = create()( - devtools( - (set, get) => ({ - ...initialState, - - setTools: (tools) => { - logger.info(`Synced ${tools.length} custom tools`) - set({ tools }) - }, - - getTool: (id: string) => { - return get().tools.find((tool) => tool.id === id) - }, - - getAllTools: () => { - return get().tools - }, - - reset: () => set(initialState), - }), - { - name: 'custom-tools-store', - } - ) -) diff --git a/apps/sim/stores/custom-tools/types.ts b/apps/sim/stores/custom-tools/types.ts deleted file mode 100644 index d9ebf6fddd..0000000000 --- a/apps/sim/stores/custom-tools/types.ts +++ /dev/null @@ -1,36 +0,0 @@ -export interface CustomToolSchema { - type: string - function: { - name: string - description?: string - parameters: { - type: string - properties: Record - required?: string[] - } - } -} - -export interface CustomToolDefinition { - id: string - workspaceId: string | null - userId: string | null - title: string - schema: CustomToolSchema - code: string - createdAt: string - updatedAt?: string -} - -export interface CustomToolsState { - tools: CustomToolDefinition[] -} - -export interface CustomToolsActions { - setTools: (tools: CustomToolDefinition[]) => void - getTool: (id: string) => CustomToolDefinition | undefined - getAllTools: () => CustomToolDefinition[] - reset: () => void -} - -export interface CustomToolsStore extends CustomToolsState, CustomToolsActions {} diff --git a/apps/sim/stores/index.ts b/apps/sim/stores/index.ts index 59ec1716dc..7e76c023dd 100644 --- a/apps/sim/stores/index.ts +++ b/apps/sim/stores/index.ts @@ -2,7 +2,6 @@ import { useEffect } from 'react' import { createLogger } from '@sim/logger' -import { useCustomToolsStore } from '@/stores/custom-tools' import { useExecutionStore } from '@/stores/execution' import { useCopilotStore, useVariablesStore } from '@/stores/panel' import { useEnvironmentStore } from '@/stores/settings/environment' @@ -195,7 +194,6 @@ export { useExecutionStore, useTerminalConsoleStore, useCopilotStore, - useCustomToolsStore, useVariablesStore, useSubBlockStore, } @@ -222,7 +220,7 @@ export const resetAllStores = () => { useExecutionStore.getState().reset() useTerminalConsoleStore.setState({ entries: [], isOpen: false }) useCopilotStore.setState({ messages: [], isSendingMessage: false, error: null }) - useCustomToolsStore.getState().reset() + // Custom tools are managed by React Query cache, not a Zustand store // Variables store has no tracking to reset; registry hydrates } @@ -235,7 +233,6 @@ export const logAllStores = () => { execution: useExecutionStore.getState(), console: useTerminalConsoleStore.getState(), copilot: useCopilotStore.getState(), - customTools: useCustomToolsStore.getState(), subBlock: useSubBlockStore.getState(), variables: useVariablesStore.getState(), } diff --git a/apps/sim/tools/index.test.ts b/apps/sim/tools/index.test.ts index c4b5aefb4e..37556793bf 100644 --- a/apps/sim/tools/index.test.ts +++ b/apps/sim/tools/index.test.ts @@ -14,6 +14,54 @@ import { type MockFetchResponse, } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +// Mock custom tools query - must be hoisted before imports +vi.mock('@/hooks/queries/custom-tools', () => ({ + getCustomTool: (toolId: string) => { + if (toolId === 'custom-tool-123') { + return { + id: 'custom-tool-123', + title: 'Custom Weather Tool', + code: 'return { result: "Weather data" }', + schema: { + function: { + description: 'Get weather information', + parameters: { + type: 'object', + properties: { + location: { type: 'string', description: 'City name' }, + unit: { type: 'string', description: 'Unit (metric/imperial)' }, + }, + required: ['location'], + }, + }, + }, + } + } + return undefined + }, + getCustomTools: () => [ + { + id: 'custom-tool-123', + title: 'Custom Weather Tool', + code: 'return { result: "Weather data" }', + schema: { + function: { + description: 'Get weather information', + parameters: { + type: 'object', + properties: { + location: { type: 'string', description: 'City name' }, + unit: { type: 'string', description: 'Unit (metric/imperial)' }, + }, + required: ['location'], + }, + }, + }, + }, + ], +})) + import { executeTool } from '@/tools/index' import { tools } from '@/tools/registry' import { getTool } from '@/tools/utils' @@ -94,73 +142,6 @@ describe('Tools Registry', () => { }) describe('Custom Tools', () => { - beforeEach(() => { - vi.mock('@/stores/custom-tools', () => ({ - useCustomToolsStore: { - getState: () => ({ - getTool: (id: string) => { - if (id === 'custom-tool-123') { - return { - id: 'custom-tool-123', - title: 'Custom Weather Tool', - code: 'return { result: "Weather data" }', - schema: { - function: { - description: 'Get weather information', - parameters: { - type: 'object', - properties: { - location: { type: 'string', description: 'City name' }, - unit: { type: 'string', description: 'Unit (metric/imperial)' }, - }, - required: ['location'], - }, - }, - }, - } - } - return undefined - }, - getAllTools: () => [ - { - id: 'custom-tool-123', - title: 'Custom Weather Tool', - code: 'return { result: "Weather data" }', - schema: { - function: { - description: 'Get weather information', - parameters: { - type: 'object', - properties: { - location: { type: 'string', description: 'City name' }, - unit: { type: 'string', description: 'Unit (metric/imperial)' }, - }, - required: ['location'], - }, - }, - }, - }, - ], - }), - }, - })) - - vi.mock('@/stores/settings/environment', () => ({ - useEnvironmentStore: { - getState: () => ({ - getAllVariables: () => ({ - API_KEY: { value: 'test-api-key' }, - BASE_URL: { value: 'https://test-base-url.com' }, - }), - }), - }, - })) - }) - - afterEach(() => { - vi.resetAllMocks() - }) - it('should get custom tool by ID', () => { const customTool = getTool('custom_custom-tool-123') expect(customTool).toBeDefined() diff --git a/apps/sim/tools/utils.ts b/apps/sim/tools/utils.ts index 9e09d90c5a..8dc30ddc79 100644 --- a/apps/sim/tools/utils.ts +++ b/apps/sim/tools/utils.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { getBaseUrl } from '@/lib/core/utils/urls' import { AGENT, isCustomTool } from '@/executor/constants' -import { useCustomToolsStore } from '@/stores/custom-tools' +import { getCustomTool } from '@/hooks/queries/custom-tools' import { useEnvironmentStore } from '@/stores/settings/environment' import { extractErrorMessage } from '@/tools/error-extractors' import { tools } from '@/tools/registry' @@ -335,17 +335,10 @@ export function getTool(toolId: string): ToolConfig | undefined { // Check if it's a custom tool if (isCustomTool(toolId) && typeof window !== 'undefined') { // Only try to use the sync version on the client - const customToolsStore = useCustomToolsStore.getState() const identifier = toolId.slice(AGENT.CUSTOM_TOOL_PREFIX.length) - // Try to find the tool directly by ID first - let customTool = customToolsStore.getTool(identifier) - - // If not found by ID, try to find by title (for backward compatibility) - if (!customTool) { - const allTools = customToolsStore.getAllTools() - customTool = allTools.find((tool) => tool.title === identifier) - } + // Try to find the tool from query cache (extracts workspaceId from URL) + const customTool = getCustomTool(identifier) if (customTool) { return createToolConfig(customTool, toolId) @@ -367,7 +360,7 @@ export async function getToolAsync( // Check if it's a custom tool if (isCustomTool(toolId)) { - return getCustomTool(toolId, workflowId) + return fetchCustomToolFromAPI(toolId, workflowId) } return undefined @@ -411,8 +404,8 @@ function createToolConfig(customTool: any, customToolId: string): ToolConfig { } } -// Create a tool config from a custom tool definition -async function getCustomTool( +// Create a tool config from a custom tool definition by fetching from API +async function fetchCustomToolFromAPI( customToolId: string, workflowId?: string ): Promise {