Skip to content
Merged
Binary file modified apps/docs/public/static/introduction.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -48,6 +65,7 @@ interface PageItem {
href?: string
onClick?: () => void
shortcut?: string
hidden?: boolean
}

export function SearchModal({
Expand All @@ -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<HTMLInputElement>(null)
const [search, setSearch] = useState('')
const [mounted, setMounted] = useState(false)
const openSettingsModal = useSettingsModalStore((state) => state.openModal)
const { config: permissionConfig } = usePermissionConfig()

useEffect(() => {
setMounted(true)
Expand All @@ -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()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overly complex DOM manipulation to clear input

Low Severity

The new approach uses Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set to clear the search input, which is more complex than the previous controlled React state pattern (setSearch('')). This DOM manipulation technique relies on browser implementation details and is harder to understand and maintain than standard React state management.

Fix in Cursor Fix in Web

}
}, [open])

const handleSearchChange = useCallback((value: string) => {
setSearch(value)
const handleSearchChange = useCallback(() => {
requestAnimationFrame(() => {
const list = document.querySelector('[cmdk-list]')
if (list) {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -278,7 +285,6 @@ export function SearchModal({
<Command label='Search' filter={customFilter}>
<Command.Input
ref={inputRef}
value={search}
autoFocus
onValueChange={handleSearchChange}
placeholder='Search anything...'
Expand All @@ -295,7 +301,6 @@ export function SearchModal({
<CommandItem
key={block.id}
value={`${block.name} block-${block.id}`}
keywords={[block.description]}
onSelect={() => handleBlockSelect(block, 'block')}
icon={block.icon}
bgColor={block.bgColor}
Expand All @@ -313,7 +318,6 @@ export function SearchModal({
<CommandItem
key={tool.id}
value={`${tool.name} tool-${tool.id}`}
keywords={[tool.description]}
onSelect={() => handleBlockSelect(tool, 'tool')}
icon={tool.icon}
bgColor={tool.bgColor}
Expand All @@ -331,7 +335,6 @@ export function SearchModal({
<CommandItem
key={trigger.id}
value={`${trigger.name} trigger-${trigger.id}`}
keywords={[trigger.description]}
onSelect={() => handleBlockSelect(trigger, 'trigger')}
icon={trigger.icon}
bgColor={trigger.bgColor}
Expand Down Expand Up @@ -371,7 +374,6 @@ export function SearchModal({
<CommandItem
key={op.id}
value={`${op.searchValue} operation-${op.id}`}
keywords={op.keywords}
onSelect={() => handleToolOperationSelect(op)}
icon={op.icon}
bgColor={op.bgColor}
Expand Down Expand Up @@ -458,7 +460,6 @@ const groupHeadingClassName =

interface CommandItemProps {
value: string
keywords?: string[]
onSelect: () => void
icon: React.ComponentType<{ className?: string }>
bgColor: string
Expand All @@ -468,7 +469,6 @@ interface CommandItemProps {

function CommandItem({
value,
keywords,
onSelect,
icon: Icon,
bgColor,
Expand All @@ -478,7 +478,6 @@ function CommandItem({
return (
<Command.Item
value={value}
keywords={keywords}
onSelect={onSelect}
className='group flex h-[28px] w-full cursor-pointer items-center gap-[8px] rounded-[6px] px-[10px] text-left text-[15px] aria-selected:bg-[var(--border)] aria-selected:shadow-sm data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50'
>
Expand Down
10 changes: 10 additions & 0 deletions apps/sim/blocks/blocks/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand All @@ -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)' },
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/core/security/input-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Loading