diff --git a/apps/docs/public/static/introduction.png b/apps/docs/public/static/introduction.png index d6d14cb3b..598c843ed 100644 Binary files a/apps/docs/public/static/introduction.png and b/apps/docs/public/static/introduction.png differ diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index a08a5833a..c52e247ff 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -180,20 +180,6 @@ function resolveCustomToolFromReference( return null } -/** - * Checks if a stored custom tool uses the reference-only format. - * - * @remarks - * Reference-only format means the tool has a customToolId but no inline code/schema, - * requiring resolution from the database at runtime. - * - * @param storedTool - The stored tool to check - * @returns `true` if the tool is a reference-only custom tool, `false` otherwise - */ -function isCustomToolReference(storedTool: StoredTool): boolean { - return storedTool.type === 'custom-tool' && !!storedTool.customToolId && !storedTool.code -} - /** * Generic sync wrapper that synchronizes store values with local component state. * @@ -1155,21 +1141,6 @@ export const ToolInput = memo(function ToolInput({ return filterBlocks(allToolBlocks) }, [filterBlocks]) - const customFilter = useCallback((value: string, search: string) => { - if (!search.trim()) return 1 - - const normalizedValue = value.toLowerCase() - const normalizedSearch = search.toLowerCase() - - if (normalizedValue === normalizedSearch) return 1 - - if (normalizedValue.startsWith(normalizedSearch)) return 0.8 - - if (normalizedValue.includes(normalizedSearch)) return 0.6 - - return 0 - }, []) - const hasBackfilledRef = useRef(false) useEffect(() => { if ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx index 163eb49f4..5a950c497 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx @@ -6,10 +6,10 @@ import { Database, HelpCircle, Layout, Settings } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' import { createPortal } from 'react-dom' import { Library } from '@/components/emcn' -import { useBrandConfig } from '@/lib/branding/branding' import { cn } from '@/lib/core/utils/cn' import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils' import { SIDEBAR_SCROLL_EVENT } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar' +import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSearchModalStore } from '@/stores/modals/search/store' import type { SearchBlockItem, @@ -18,6 +18,23 @@ import type { } from '@/stores/modals/search/types' import { useSettingsModalStore } from '@/stores/modals/settings/store' +function customFilter(value: string, search: string): number { + const searchLower = search.toLowerCase() + const valueLower = value.toLowerCase() + + if (valueLower === searchLower) return 1 + if (valueLower.startsWith(searchLower)) return 0.9 + if (valueLower.includes(searchLower)) return 0.7 + + const searchWords = searchLower.split(/\s+/).filter(Boolean) + if (searchWords.length > 1) { + const allWordsMatch = searchWords.every((word) => valueLower.includes(word)) + if (allWordsMatch) return 0.5 + } + + return 0 +} + interface SearchModalProps { open: boolean onOpenChange: (open: boolean) => void @@ -48,6 +65,7 @@ interface PageItem { href?: string onClick?: () => void shortcut?: string + hidden?: boolean } export function SearchModal({ @@ -60,11 +78,10 @@ export function SearchModal({ const params = useParams() const router = useRouter() const workspaceId = params.workspaceId as string - const brand = useBrandConfig() const inputRef = useRef(null) - const [search, setSearch] = useState('') const [mounted, setMounted] = useState(false) const openSettingsModal = useSettingsModalStore((state) => state.openModal) + const { config: permissionConfig } = usePermissionConfig() useEffect(() => { setMounted(true) @@ -79,54 +96,66 @@ export function SearchModal({ }, []) const pages = useMemo( - (): PageItem[] => [ - { - id: 'logs', - name: 'Logs', - icon: Library, - href: `/workspace/${workspaceId}/logs`, - shortcut: '⌘⇧L', - }, - { - id: 'templates', - name: 'Templates', - icon: Layout, - href: `/workspace/${workspaceId}/templates`, - }, - { - id: 'knowledge-base', - name: 'Knowledge Base', - icon: Database, - href: `/workspace/${workspaceId}/knowledge`, - }, - { - id: 'help', - name: 'Help', - icon: HelpCircle, - onClick: openHelpModal, - }, - { - id: 'settings', - name: 'Settings', - icon: Settings, - onClick: openSettingsModal, - shortcut: '⌘,', - }, - ], - [workspaceId, openHelpModal, openSettingsModal] + (): PageItem[] => + [ + { + id: 'logs', + name: 'Logs', + icon: Library, + href: `/workspace/${workspaceId}/logs`, + shortcut: '⌘⇧L', + }, + { + id: 'templates', + name: 'Templates', + icon: Layout, + href: `/workspace/${workspaceId}/templates`, + hidden: permissionConfig.hideTemplates, + }, + { + id: 'knowledge-base', + name: 'Knowledge Base', + icon: Database, + href: `/workspace/${workspaceId}/knowledge`, + hidden: permissionConfig.hideKnowledgeBaseTab, + }, + { + id: 'help', + name: 'Help', + icon: HelpCircle, + onClick: openHelpModal, + }, + { + id: 'settings', + name: 'Settings', + icon: Settings, + onClick: openSettingsModal, + }, + ].filter((page) => !page.hidden), + [ + workspaceId, + openHelpModal, + openSettingsModal, + permissionConfig.hideTemplates, + permissionConfig.hideKnowledgeBaseTab, + ] ) useEffect(() => { - if (open) { - setSearch('') - requestAnimationFrame(() => { - inputRef.current?.focus() - }) + if (open && inputRef.current) { + const nativeInputValueSetter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + 'value' + )?.set + if (nativeInputValueSetter) { + nativeInputValueSetter.call(inputRef.current, '') + inputRef.current.dispatchEvent(new Event('input', { bubbles: true })) + } + inputRef.current.focus() } }, [open]) - const handleSearchChange = useCallback((value: string) => { - setSearch(value) + const handleSearchChange = useCallback(() => { requestAnimationFrame(() => { const list = document.querySelector('[cmdk-list]') if (list) { @@ -228,28 +257,6 @@ export function SearchModal({ const showToolOperations = isOnWorkflowPage && toolOperations.length > 0 const showDocs = isOnWorkflowPage && docs.length > 0 - const customFilter = useCallback((value: string, search: string, keywords?: string[]) => { - const searchLower = search.toLowerCase() - const valueLower = value.toLowerCase() - - if (valueLower === searchLower) return 1 - if (valueLower.startsWith(searchLower)) return 0.8 - if (valueLower.includes(searchLower)) return 0.6 - - const searchWords = searchLower.split(/\s+/).filter(Boolean) - const allWordsMatch = searchWords.every((word) => valueLower.includes(word)) - if (allWordsMatch && searchWords.length > 0) return 0.4 - - if (keywords?.length) { - const keywordsLower = keywords.join(' ').toLowerCase() - if (keywordsLower.includes(searchLower)) return 0.3 - const keywordWordsMatch = searchWords.every((word) => keywordsLower.includes(word)) - if (keywordWordsMatch && searchWords.length > 0) return 0.2 - } - - return 0 - }, []) - if (!mounted) return null return createPortal( @@ -278,7 +285,6 @@ export function SearchModal({ handleBlockSelect(block, 'block')} icon={block.icon} bgColor={block.bgColor} @@ -313,7 +318,6 @@ export function SearchModal({ handleBlockSelect(tool, 'tool')} icon={tool.icon} bgColor={tool.bgColor} @@ -331,7 +335,6 @@ export function SearchModal({ handleBlockSelect(trigger, 'trigger')} icon={trigger.icon} bgColor={trigger.bgColor} @@ -371,7 +374,6 @@ export function SearchModal({ handleToolOperationSelect(op)} icon={op.icon} bgColor={op.bgColor} @@ -458,7 +460,6 @@ const groupHeadingClassName = interface CommandItemProps { value: string - keywords?: string[] onSelect: () => void icon: React.ComponentType<{ className?: string }> bgColor: string @@ -468,7 +469,6 @@ interface CommandItemProps { function CommandItem({ value, - keywords, onSelect, icon: Icon, bgColor, @@ -478,7 +478,6 @@ function CommandItem({ return ( diff --git a/apps/sim/blocks/blocks/api.ts b/apps/sim/blocks/blocks/api.ts index 50ee12eb3..091454621 100644 --- a/apps/sim/blocks/blocks/api.ts +++ b/apps/sim/blocks/blocks/api.ts @@ -80,6 +80,15 @@ Example: generationType: 'json-object', }, }, + { + id: 'timeout', + title: 'Timeout (ms)', + type: 'short-input', + placeholder: '300000', + description: + 'Request timeout in milliseconds (default: 300000 = 5 minutes, max: 600000 = 10 minutes)', + mode: 'advanced', + }, ], tools: { access: ['http_request'], @@ -90,6 +99,7 @@ Example: headers: { type: 'json', description: 'Request headers' }, body: { type: 'json', description: 'Request body data' }, params: { type: 'json', description: 'URL query parameters' }, + timeout: { type: 'number', description: 'Request timeout in milliseconds' }, }, outputs: { data: { type: 'json', description: 'API response data (JSON, text, or other formats)' }, diff --git a/apps/sim/lib/core/security/input-validation.ts b/apps/sim/lib/core/security/input-validation.ts index abeb8f6bd..e27524b2c 100644 --- a/apps/sim/lib/core/security/input-validation.ts +++ b/apps/sim/lib/core/security/input-validation.ts @@ -931,7 +931,7 @@ export async function secureFetchWithPinnedIP( method: options.method || 'GET', headers: sanitizedHeaders, agent, - timeout: options.timeout || 30000, + timeout: options.timeout || 300000, // Default 5 minutes } const protocol = isHttps ? https : http @@ -1011,7 +1011,7 @@ export async function secureFetchWithPinnedIP( req.on('timeout', () => { req.destroy() - reject(new Error('Request timeout')) + reject(new Error(`Request timed out after ${requestOptions.timeout}ms`)) }) if (options.body) { diff --git a/apps/sim/stores/modals/search/store.ts b/apps/sim/stores/modals/search/store.ts index ac591e7b3..210dc97c1 100644 --- a/apps/sim/stores/modals/search/store.ts +++ b/apps/sim/stores/modals/search/store.ts @@ -23,7 +23,7 @@ const initialData: SearchData = { export const useSearchModalStore = create()( devtools( - (set, get) => ({ + (set, _) => ({ isOpen: false, data: initialData, @@ -53,7 +53,6 @@ export const useSearchModalStore = create()( const searchItem: SearchBlockItem = { id: block.type, name: block.name, - description: block.description || '', icon: block.icon, bgColor: block.bgColor || '#6B7280', type: block.type, @@ -79,7 +78,6 @@ export const useSearchModalStore = create()( { id: 'loop', name: 'Loop', - description: 'Create a Loop', icon: RepeatIcon, bgColor: '#2FB3FF', type: 'loop', @@ -87,7 +85,6 @@ export const useSearchModalStore = create()( { id: 'parallel', name: 'Parallel', - description: 'Parallel Execution', icon: SplitIcon, bgColor: '#FEE12B', type: 'parallel', @@ -116,7 +113,6 @@ export const useSearchModalStore = create()( (block): SearchBlockItem => ({ id: block.type, name: block.name, - description: block.description || '', icon: block.icon, bgColor: block.bgColor || '#6B7280', type: block.type, @@ -127,16 +123,18 @@ export const useSearchModalStore = create()( const allowedBlockTypes = new Set(tools.map((t) => t.type)) const toolOperations: SearchToolOperationItem[] = getToolOperationsIndex() .filter((op) => allowedBlockTypes.has(op.blockType)) - .map((op) => ({ - id: op.id, - name: op.operationName, - searchValue: `${op.serviceName} ${op.operationName}`, - icon: op.icon, - bgColor: op.bgColor, - blockType: op.blockType, - operationId: op.operationId, - keywords: op.aliases, - })) + .map((op) => { + const aliasesStr = op.aliases?.length ? ` ${op.aliases.join(' ')}` : '' + return { + id: op.id, + name: op.operationName, + searchValue: `${op.serviceName} ${op.operationName}${aliasesStr}`, + icon: op.icon, + bgColor: op.bgColor, + blockType: op.blockType, + operationId: op.operationId, + } + }) set({ data: { diff --git a/apps/sim/stores/modals/search/types.ts b/apps/sim/stores/modals/search/types.ts index 07dde9d09..ead6c2cb6 100644 --- a/apps/sim/stores/modals/search/types.ts +++ b/apps/sim/stores/modals/search/types.ts @@ -7,7 +7,6 @@ import type { BlockConfig } from '@/blocks/types' export interface SearchBlockItem { id: string name: string - description: string icon: ComponentType<{ className?: string }> bgColor: string type: string @@ -25,7 +24,6 @@ export interface SearchToolOperationItem { bgColor: string blockType: string operationId: string - keywords: string[] } /** diff --git a/apps/sim/tools/http/request.ts b/apps/sim/tools/http/request.ts index eae6ab94a..dbc74df4d 100644 --- a/apps/sim/tools/http/request.ts +++ b/apps/sim/tools/http/request.ts @@ -48,6 +48,11 @@ export const requestTool: ToolConfig = { visibility: 'user-or-llm', description: 'Form data to send (will set appropriate Content-Type)', }, + timeout: { + type: 'number', + visibility: 'user-only', + description: 'Request timeout in milliseconds (default: 300000 = 5 minutes)', + }, }, request: { diff --git a/apps/sim/tools/http/types.ts b/apps/sim/tools/http/types.ts index e80a2bf02..3799fd0ca 100644 --- a/apps/sim/tools/http/types.ts +++ b/apps/sim/tools/http/types.ts @@ -8,6 +8,7 @@ export interface RequestParams { params?: TableRow[] pathParams?: Record formData?: Record + timeout?: number } export interface RequestResponse extends ToolResponse { diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index c6647680e..3b1c0f15b 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -625,11 +625,27 @@ async function executeToolRequest( let response: Response if (isInternalRoute) { - response = await fetch(fullUrl, { - method: requestParams.method, - headers: headers, - body: requestParams.body, - }) + // Set up AbortController for timeout support on internal routes + const controller = new AbortController() + const timeout = requestParams.timeout || 300000 + const timeoutId = setTimeout(() => controller.abort(), timeout) + + try { + response = await fetch(fullUrl, { + method: requestParams.method, + headers: headers, + body: requestParams.body, + signal: controller.signal, + }) + } catch (error) { + // Convert AbortError to a timeout error message + if (error instanceof Error && error.name === 'AbortError') { + throw new Error(`Request timed out after ${timeout}ms`) + } + throw error + } finally { + clearTimeout(timeoutId) + } } else { const urlValidation = await validateUrlWithDNS(fullUrl, 'toolUrl') if (!urlValidation.isValid) { @@ -640,6 +656,7 @@ async function executeToolRequest( method: requestParams.method, headers: headersRecord, body: requestParams.body ?? undefined, + timeout: requestParams.timeout, }) const responseHeaders = new Headers(secureResponse.headers.toRecord()) diff --git a/apps/sim/tools/utils.ts b/apps/sim/tools/utils.ts index 6d9d27601..12ab81772 100644 --- a/apps/sim/tools/utils.ts +++ b/apps/sim/tools/utils.ts @@ -75,6 +75,7 @@ interface RequestParams { method: string headers: Record body?: string + timeout?: number } /** @@ -122,7 +123,17 @@ export function formatRequestParams(tool: ToolConfig, params: Record 0 + ? Math.min(timeout, MAX_TIMEOUT_MS) + : undefined + + return { url, method, headers, body, timeout: validTimeout } } /**