diff --git a/apps/docs/content/docs/en/tools/google_books.mdx b/apps/docs/content/docs/en/tools/google_books.mdx index 9baec6846a..2b370d1394 100644 --- a/apps/docs/content/docs/en/tools/google_books.mdx +++ b/apps/docs/content/docs/en/tools/google_books.mdx @@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" ## Usage Instructions diff --git a/apps/docs/content/docs/en/tools/s3.mdx b/apps/docs/content/docs/en/tools/s3.mdx index 95715f0f19..f7780eb585 100644 --- a/apps/docs/content/docs/en/tools/s3.mdx +++ b/apps/docs/content/docs/en/tools/s3.mdx @@ -71,6 +71,7 @@ Retrieve an object from an AWS S3 bucket | --------- | ---- | -------- | ----------- | | `accessKeyId` | string | Yes | Your AWS Access Key ID | | `secretAccessKey` | string | Yes | Your AWS Secret Access Key | +| `region` | string | No | Optional region override when URL does not include region \(e.g., us-east-1, eu-west-1\) | | `s3Uri` | string | Yes | S3 Object URL \(e.g., https://bucket.s3.region.amazonaws.com/path/to/file\) | #### Output diff --git a/apps/docs/content/docs/en/tools/slack.mdx b/apps/docs/content/docs/en/tools/slack.mdx index 35562a17ed..0f4285e2a1 100644 --- a/apps/docs/content/docs/en/tools/slack.mdx +++ b/apps/docs/content/docs/en/tools/slack.mdx @@ -79,7 +79,7 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format | `channel` | string | No | Slack channel ID \(e.g., C1234567890\) | | `dmUserId` | string | No | Slack user ID for direct messages \(e.g., U1234567890\) | | `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) | -| `thread_ts` | string | No | Thread timestamp to reply to \(creates thread reply\) | +| `threadTs` | string | No | Thread timestamp to reply to \(creates thread reply\) | | `files` | file[] | No | Files to attach to the message | #### Output diff --git a/apps/sim/app/api/wand/route.ts b/apps/sim/app/api/wand/route.ts index aba3b14eff..f6089ff1d2 100644 --- a/apps/sim/app/api/wand/route.ts +++ b/apps/sim/app/api/wand/route.ts @@ -238,6 +238,11 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg finalSystemPrompt += currentTimeContext } + if (generationType === 'cron-expression') { + finalSystemPrompt += + '\n\nIMPORTANT: Return ONLY the raw cron expression (e.g., "0 9 * * 1-5"). Do NOT wrap it in markdown code blocks, backticks, or quotes. Do NOT include any explanation or text before or after the expression.' + } + if (generationType === 'json-object') { finalSystemPrompt += '\n\nIMPORTANT: Return ONLY the raw JSON object. Do NOT wrap it in markdown code blocks (no ```json or ```). Do NOT include any explanation or text before or after the JSON. The response must start with { and end with }.' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/status-bar/index.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/status-bar/index.ts index e3f74828ba..1e4a78adfa 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/status-bar/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/status-bar/index.ts @@ -1,2 +1,2 @@ export type { StatusBarSegment } from './status-bar' -export { default, StatusBar } from './status-bar' +export { StatusBar } from './status-bar' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/status-bar/status-bar.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/status-bar/status-bar.tsx index 201d7c97b0..568d4179c2 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/status-bar/status-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/status-bar/status-bar.tsx @@ -8,7 +8,7 @@ export interface StatusBarSegment { timestamp: string } -export function StatusBar({ +function StatusBarInner({ segments, selectedSegmentIndices, onSegmentClick, @@ -127,4 +127,45 @@ export function StatusBar({ ) } -export default memo(StatusBar) +/** + * Custom equality function for StatusBar memo. + * Performs structural comparison of segments array to avoid re-renders + * when poll data returns new object references with identical content. + */ +function areStatusBarPropsEqual( + prev: Parameters[0], + next: Parameters[0] +): boolean { + if (prev.workflowId !== next.workflowId) return false + if (prev.segmentDurationMs !== next.segmentDurationMs) return false + if (prev.preferBelow !== next.preferBelow) return false + + if (prev.selectedSegmentIndices !== next.selectedSegmentIndices) { + if (!prev.selectedSegmentIndices || !next.selectedSegmentIndices) return false + if (prev.selectedSegmentIndices.length !== next.selectedSegmentIndices.length) return false + for (let i = 0; i < prev.selectedSegmentIndices.length; i++) { + if (prev.selectedSegmentIndices[i] !== next.selectedSegmentIndices[i]) return false + } + } + + if (prev.segments !== next.segments) { + if (prev.segments.length !== next.segments.length) return false + for (let i = 0; i < prev.segments.length; i++) { + const ps = prev.segments[i] + const ns = next.segments[i] + if ( + ps.successRate !== ns.successRate || + ps.hasExecutions !== ns.hasExecutions || + ps.totalExecutions !== ns.totalExecutions || + ps.successfulExecutions !== ns.successfulExecutions || + ps.timestamp !== ns.timestamp + ) { + return false + } + } + } + + return true +} + +export const StatusBar = memo(StatusBarInner, areStatusBarPropsEqual) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/workflows-list/index.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/workflows-list/index.ts index 22f40a871a..cae73033cd 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/workflows-list/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/workflows-list/index.ts @@ -1,2 +1,2 @@ export type { WorkflowExecutionItem } from './workflows-list' -export { default, WorkflowsList } from './workflows-list' +export { WorkflowsList } from './workflows-list' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/workflows-list/workflows-list.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/workflows-list/workflows-list.tsx index 5904bd0365..9700f4f8bf 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/workflows-list/workflows-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/workflows-list/workflows-list.tsx @@ -14,7 +14,7 @@ export interface WorkflowExecutionItem { overallSuccessRate: number } -export function WorkflowsList({ +function WorkflowsListInner({ filteredExecutions, expandedWorkflowId, onToggleWorkflow, @@ -103,7 +103,7 @@ export function WorkflowsList({ >({}) const [lastAnchorIndices, setLastAnchorIndices] = useState>({}) - const barsAreaRef = useRef(null) + const lastAnchorIndicesRef = useRef>({}) const { workflowIds, searchQuery, toggleWorkflowId, timeRange } = useFilterStore() @@ -152,20 +152,79 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) { const expandedWorkflowId = workflowIds.length === 1 ? workflowIds[0] : null - const { executions, aggregateSegments, segmentMs } = useMemo(() => { + const { rawExecutions, aggregateSegments, segmentMs } = useMemo(() => { if (!stats) { - return { executions: [], aggregateSegments: [], segmentMs: 0 } + return { rawExecutions: [], aggregateSegments: [], segmentMs: 0 } } - const workflowExecutions = stats.workflows.map(toWorkflowExecution) - return { - executions: workflowExecutions, + rawExecutions: stats.workflows.map(toWorkflowExecution), aggregateSegments: stats.aggregateSegments, segmentMs: stats.segmentMs, } }, [stats]) + /** + * Stabilize execution objects: reuse previous references for workflows + * whose segment data hasn't structurally changed between polls. + * This prevents cascading re-renders through WorkflowsList → StatusBar. + */ + const prevExecutionsRef = useRef([]) + + const executions = useMemo(() => { + const prevMap = new Map(prevExecutionsRef.current.map((e) => [e.workflowId, e])) + let anyChanged = false + + const result = rawExecutions.map((exec) => { + const prev = prevMap.get(exec.workflowId) + if (!prev) { + anyChanged = true + return exec + } + if ( + prev.overallSuccessRate !== exec.overallSuccessRate || + prev.workflowName !== exec.workflowName || + prev.segments.length !== exec.segments.length + ) { + anyChanged = true + return exec + } + + for (let i = 0; i < prev.segments.length; i++) { + const ps = prev.segments[i] + const ns = exec.segments[i] + if ( + ps.totalExecutions !== ns.totalExecutions || + ps.successfulExecutions !== ns.successfulExecutions || + ps.timestamp !== ns.timestamp || + ps.avgDurationMs !== ns.avgDurationMs || + ps.p50Ms !== ns.p50Ms || + ps.p90Ms !== ns.p90Ms || + ps.p99Ms !== ns.p99Ms + ) { + anyChanged = true + return exec + } + } + + return prev + }) + + if ( + !anyChanged && + result.length === prevExecutionsRef.current.length && + result.every((r, i) => r === prevExecutionsRef.current[i]) + ) { + return prevExecutionsRef.current + } + + return result + }, [rawExecutions]) + + useEffect(() => { + prevExecutionsRef.current = executions + }, [executions]) + const lastExecutionByWorkflow = useMemo(() => { const map = new Map() for (const wf of executions) { @@ -312,6 +371,8 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) { [toggleWorkflowId] ) + lastAnchorIndicesRef.current = lastAnchorIndices + /** * Handles segment click for selecting time segments. * @param workflowId - The workflow containing the segment @@ -361,7 +422,7 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) { } else if (mode === 'range') { setSelectedSegments((prev) => { const currentSegments = prev[workflowId] || [] - const anchor = lastAnchorIndices[workflowId] ?? segmentIndex + const anchor = lastAnchorIndicesRef.current[workflowId] ?? segmentIndex const [start, end] = anchor < segmentIndex ? [anchor, segmentIndex] : [segmentIndex, anchor] const range = Array.from({ length: end - start + 1 }, (_, i) => start + i) @@ -370,12 +431,12 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) { }) } }, - [lastAnchorIndices] + [] ) useEffect(() => { - setSelectedSegments({}) - setLastAnchorIndices({}) + setSelectedSegments((prev) => (Object.keys(prev).length > 0 ? {} : prev)) + setLastAnchorIndices((prev) => (Object.keys(prev).length > 0 ? {} : prev)) }, [stats, timeRange, workflowIds, searchQuery]) if (isLoading) { @@ -493,7 +554,7 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) { -
+
) } + +export default memo(DashboardInner) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index 43aa334e48..490e69a121 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -43,184 +43,199 @@ import { useLogDetailsUIStore } from '@/stores/logs/store' /** * Workflow Output section with code viewer, copy, search, and context menu functionality */ -function WorkflowOutputSection({ output }: { output: Record }) { - const contentRef = useRef(null) - const [copied, setCopied] = useState(false) - - // Context menu state - const [isContextMenuOpen, setIsContextMenuOpen] = useState(false) - const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }) - - const { - isSearchActive, - searchQuery, - setSearchQuery, - matchCount, - currentMatchIndex, - activateSearch, - closeSearch, - goToNextMatch, - goToPreviousMatch, - handleMatchCountChange, - searchInputRef, - } = useCodeViewerFeatures({ contentRef }) - - const jsonString = useMemo(() => JSON.stringify(output, null, 2), [output]) - - const handleContextMenu = useCallback((e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() - setContextMenuPosition({ x: e.clientX, y: e.clientY }) - setIsContextMenuOpen(true) - }, []) - - const closeContextMenu = useCallback(() => { - setIsContextMenuOpen(false) - }, []) - - const handleCopy = useCallback(() => { - navigator.clipboard.writeText(jsonString) - setCopied(true) - setTimeout(() => setCopied(false), 1500) - closeContextMenu() - }, [jsonString, closeContextMenu]) - - const handleSearch = useCallback(() => { - activateSearch() - closeContextMenu() - }, [activateSearch, closeContextMenu]) +const WorkflowOutputSection = memo( + function WorkflowOutputSection({ output }: { output: Record }) { + const contentRef = useRef(null) + const [copied, setCopied] = useState(false) + const copyTimerRef = useRef(null) + + const [isContextMenuOpen, setIsContextMenuOpen] = useState(false) + const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }) + + const { + isSearchActive, + searchQuery, + setSearchQuery, + matchCount, + currentMatchIndex, + activateSearch, + closeSearch, + goToNextMatch, + goToPreviousMatch, + handleMatchCountChange, + searchInputRef, + } = useCodeViewerFeatures({ contentRef }) + + const jsonString = useMemo(() => JSON.stringify(output, null, 2), [output]) + + const handleContextMenu = useCallback((e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + setContextMenuPosition({ x: e.clientX, y: e.clientY }) + setIsContextMenuOpen(true) + }, []) + + const closeContextMenu = useCallback(() => { + setIsContextMenuOpen(false) + }, []) + + const handleCopy = useCallback(() => { + navigator.clipboard.writeText(jsonString) + setCopied(true) + if (copyTimerRef.current !== null) window.clearTimeout(copyTimerRef.current) + copyTimerRef.current = window.setTimeout(() => setCopied(false), 1500) + closeContextMenu() + }, [jsonString, closeContextMenu]) + + useEffect(() => { + return () => { + if (copyTimerRef.current !== null) window.clearTimeout(copyTimerRef.current) + } + }, []) - return ( -
-
- - {/* Glass action buttons overlay */} - {!isSearchActive && ( -
- - - - - {copied ? 'Copied' : 'Copy'} - - - - - - Search - -
- )} -
+ const handleSearch = useCallback(() => { + activateSearch() + closeContextMenu() + }, [activateSearch, closeContextMenu]) - {/* Search Overlay */} - {isSearchActive && ( -
e.stopPropagation()} - > - setSearchQuery(e.target.value)} - placeholder='Search...' - className='mr-[2px] h-[23px] w-[94px] text-[12px]' + return ( +
+
+ - 0 ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]' - )} - > - {matchCount > 0 ? `${currentMatchIndex + 1}/${matchCount}` : '0/0'} - - - - + {/* Glass action buttons overlay */} + {!isSearchActive && ( +
+ + + + + {copied ? 'Copied' : 'Copy'} + + + + + + Search + +
+ )}
- )} - {/* Context Menu - rendered in portal to avoid transform/overflow clipping */} - {typeof document !== 'undefined' && - createPortal( - e.stopPropagation()} > - setSearchQuery(e.target.value)} + placeholder='Search...' + className='mr-[2px] h-[23px] w-[94px] text-[12px]' /> - - Copy - - Search - - , - document.body + 0 ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]' + )} + > + {matchCount > 0 ? `${currentMatchIndex + 1}/${matchCount}` : '0/0'} + + + + +
)} -
- ) -} + + {/* Context Menu - rendered in portal to avoid transform/overflow clipping */} + {typeof document !== 'undefined' && + createPortal( + + + + Copy + + Search + + , + document.body + )} +
+ ) + }, + (prev, next) => prev.output === next.output +) interface LogDetailsProps { /** The log to display details for */ @@ -278,7 +293,6 @@ export const LogDetails = memo(function LogDetails({ return isWorkflowExecutionLog && log?.cost }, [log, isWorkflowExecutionLog]) - // Extract and clean the workflow final output (recursively remove hidden keys for cleaner display) const workflowOutput = useMemo(() => { const executionData = log?.executionData as | { finalOutput?: Record } diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx index 41deea199e..69075d64f6 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx @@ -1,6 +1,7 @@ 'use client' import type { RefObject } from 'react' +import { memo } from 'react' import { Popover, PopoverAnchor, @@ -29,7 +30,7 @@ interface LogRowContextMenuProps { * Context menu for log rows. * Provides quick actions for copying data, navigation, and filtering. */ -export function LogRowContextMenu({ +export const LogRowContextMenu = memo(function LogRowContextMenu({ isOpen, position, menuRef, @@ -121,4 +122,4 @@ export function LogRowContextMenu({ ) -} +}) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx index c7ae2bf610..7d917b6cd7 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx @@ -24,6 +24,7 @@ interface LogRowProps { log: WorkflowLog isSelected: boolean onClick: (log: WorkflowLog) => void + onHover?: (log: WorkflowLog) => void onContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void selectedRowRef: React.RefObject | null } @@ -33,7 +34,14 @@ interface LogRowProps { * Uses shallow comparison for the log object. */ const LogRow = memo( - function LogRow({ log, isSelected, onClick, onContextMenu, selectedRowRef }: LogRowProps) { + function LogRow({ + log, + isSelected, + onClick, + onHover, + onContextMenu, + selectedRowRef, + }: LogRowProps) { const formattedDate = useMemo(() => formatDate(log.createdAt), [log.createdAt]) const isDeletedWorkflow = !log.workflow?.id && !log.workflowId const workflowName = isDeletedWorkflow @@ -43,6 +51,8 @@ const LogRow = memo( const handleClick = useCallback(() => onClick(log), [onClick, log]) + const handleMouseEnter = useCallback(() => onHover?.(log), [onHover, log]) + const handleContextMenu = useCallback( (e: React.MouseEvent) => { if (onContextMenu) { @@ -61,6 +71,7 @@ const LogRow = memo( isSelected && 'bg-[var(--surface-3)] dark:bg-[var(--surface-4)]' )} onClick={handleClick} + onMouseEnter={handleMouseEnter} onContextMenu={handleContextMenu} >
@@ -142,7 +153,8 @@ const LogRow = memo( prevProps.log.id === nextProps.log.id && prevProps.log.duration === nextProps.log.duration && prevProps.log.status === nextProps.log.status && - prevProps.isSelected === nextProps.isSelected + prevProps.isSelected === nextProps.isSelected && + prevProps.onHover === nextProps.onHover ) } ) @@ -151,6 +163,7 @@ interface RowProps { logs: WorkflowLog[] selectedLogId: string | null onLogClick: (log: WorkflowLog) => void + onLogHover?: (log: WorkflowLog) => void onLogContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void selectedRowRef: React.RefObject isFetchingNextPage: boolean @@ -167,6 +180,7 @@ function Row({ logs, selectedLogId, onLogClick, + onLogHover, onLogContextMenu, selectedRowRef, isFetchingNextPage, @@ -198,6 +212,7 @@ function Row({ log={log} isSelected={isSelected} onClick={onLogClick} + onHover={onLogHover} onContextMenu={onLogContextMenu} selectedRowRef={isSelected ? selectedRowRef : null} /> @@ -209,6 +224,7 @@ export interface LogsListProps { logs: WorkflowLog[] selectedLogId: string | null onLogClick: (log: WorkflowLog) => void + onLogHover?: (log: WorkflowLog) => void onLogContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void selectedRowRef: React.RefObject hasNextPage: boolean @@ -227,6 +243,7 @@ export function LogsList({ logs, selectedLogId, onLogClick, + onLogHover, onLogContextMenu, selectedRowRef, hasNextPage, @@ -272,6 +289,7 @@ export function LogsList({ logs, selectedLogId, onLogClick, + onLogHover, onLogContextMenu, selectedRowRef, isFetchingNextPage, @@ -281,6 +299,7 @@ export function LogsList({ logs, selectedLogId, onLogClick, + onLogHover, onLogContextMenu, selectedRowRef, isFetchingNextPage, diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx index 81bbfa3a28..e1dcc41937 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { createLogger } from '@sim/logger' import { Plus, X } from 'lucide-react' import { @@ -113,7 +113,7 @@ function formatAlertConfigLabel(config: { } } -export function NotificationSettings({ +export const NotificationSettings = memo(function NotificationSettings({ workspaceId, open, onOpenChange, @@ -144,7 +144,7 @@ export function NotificationSettings({ slackChannelId: '', slackChannelName: '', slackAccountId: '', - useAlertRule: false, + alertRule: 'none' as AlertRule, consecutiveFailures: 3, failureRatePercent: 50, @@ -212,7 +212,7 @@ export function NotificationSettings({ slackChannelId: '', slackChannelName: '', slackAccountId: '', - useAlertRule: false, + alertRule: 'none', consecutiveFailures: 3, failureRatePercent: 50, @@ -484,7 +484,6 @@ export function NotificationSettings({ slackChannelId: subscription.slackConfig?.channelId || '', slackChannelName: subscription.slackConfig?.channelName || '', slackAccountId: subscription.slackConfig?.accountId || '', - useAlertRule: !!subscription.alertConfig, alertRule: subscription.alertConfig?.rule || 'none', consecutiveFailures: subscription.alertConfig?.consecutiveFailures || 3, failureRatePercent: subscription.alertConfig?.failureRatePercent || 50, @@ -1289,4 +1288,4 @@ export function NotificationSettings({ ) -} +}) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx index 11280214fb..9a6897990a 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useMemo, useState } from 'react' +import { memo, useCallback, useMemo, useState } from 'react' import { ArrowUp, Bell, Library, MoreHorizontal, RefreshCw } from 'lucide-react' import { useParams } from 'next/navigation' import { @@ -149,7 +149,7 @@ function getTriggerIcon( * @param props - The component props * @returns The complete logs toolbar */ -export function LogsToolbar({ +export const LogsToolbar = memo(function LogsToolbar({ viewMode, onViewModeChange, isRefreshing, @@ -749,4 +749,4 @@ export function LogsToolbar({
) -} +}) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index aa7311fa59..c7a534ef05 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -1,6 +1,7 @@ 'use client' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react' +import { useQueryClient } from '@tanstack/react-query' import { Loader2 } from 'lucide-react' import { useParams } from 'next/navigation' import { cn } from '@/lib/core/utils/cn' @@ -10,12 +11,17 @@ import { hasActiveFilters, } from '@/lib/logs/filters' import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser' +import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { useFolders } from '@/hooks/queries/folders' -import { useDashboardStats, useLogDetail, useLogsList } from '@/hooks/queries/logs' +import { + prefetchLogDetail, + useDashboardStats, + useLogDetail, + useLogsList, +} from '@/hooks/queries/logs' import { useDebounce } from '@/hooks/use-debounce' import { useFilterStore } from '@/stores/logs/filters/store' import type { WorkflowLog } from '@/stores/logs/filters/types' -import { useUserPermissionsContext } from '../providers/workspace-permissions-provider' import { Dashboard, ExecutionSnapshot, @@ -30,6 +36,38 @@ import { LOG_COLUMN_ORDER, LOG_COLUMNS } from './utils' const LOGS_PER_PAGE = 50 as const const REFRESH_SPINNER_DURATION_MS = 1000 as const +interface LogSelectionState { + selectedLogId: string | null + isSidebarOpen: boolean +} + +type LogSelectionAction = + | { type: 'TOGGLE_LOG'; logId: string } + | { type: 'SELECT_LOG'; logId: string } + | { type: 'CLOSE_SIDEBAR' } + | { type: 'TOGGLE_SIDEBAR' } + +function logSelectionReducer( + state: LogSelectionState, + action: LogSelectionAction +): LogSelectionState { + switch (action.type) { + case 'TOGGLE_LOG': + if (state.selectedLogId === action.logId && state.isSidebarOpen) { + return { selectedLogId: null, isSidebarOpen: false } + } + return { selectedLogId: action.logId, isSidebarOpen: true } + case 'SELECT_LOG': + return { ...state, selectedLogId: action.logId } + case 'CLOSE_SIDEBAR': + return { selectedLogId: null, isSidebarOpen: false } + case 'TOGGLE_SIDEBAR': + return state.selectedLogId ? { ...state, isSidebarOpen: !state.isSidebarOpen } : state + default: + return state + } +} + /** * Logs page component displaying workflow execution history. * Supports filtering, search, live updates, and detailed log inspection. @@ -60,11 +98,13 @@ export default function Logs() { setWorkspaceId(workspaceId) }, [workspaceId, setWorkspaceId]) - const [selectedLogId, setSelectedLogId] = useState(null) - const [isSidebarOpen, setIsSidebarOpen] = useState(false) + const [{ selectedLogId, isSidebarOpen }, dispatch] = useReducer(logSelectionReducer, { + selectedLogId: null, + isSidebarOpen: false, + }) const selectedRowRef = useRef(null) const loaderRef = useRef(null) - const scrollContainerRef = useRef(null) + const isInitialized = useRef(false) const [searchQuery, setSearchQuery] = useState('') @@ -82,6 +122,13 @@ export default function Logs() { const [isVisuallyRefreshing, setIsVisuallyRefreshing] = useState(false) const [isExporting, setIsExporting] = useState(false) const isSearchOpenRef = useRef(false) + const refreshTimersRef = useRef(new Set()) + const logsRef = useRef([]) + const selectedLogIndexRef = useRef(-1) + const selectedLogIdRef = useRef(null) + const logsRefetchRef = useRef<() => void>(() => {}) + const activeLogRefetchRef = useRef<() => void>(() => {}) + const logsQueryRef = useRef({ isFetching: false, hasNextPage: false, fetchNextPage: () => {} }) const [isNotificationSettingsOpen, setIsNotificationSettingsOpen] = useState(false) const userPermissions = useUserPermissionsContext() @@ -94,8 +141,19 @@ export default function Logs() { const [previewLogId, setPreviewLogId] = useState(null) const activeLogId = isPreviewOpen ? previewLogId : selectedLogId + const queryClient = useQueryClient() + + const detailRefetchInterval = useCallback( + (query: { state: { data?: WorkflowLog } }) => { + if (!isLive) return false + const status = query.state.data?.status + return status === 'running' || status === 'pending' ? 3000 : false + }, + [isLive] + ) + const activeLogQuery = useLogDetail(activeLogId ?? undefined, { - refetchInterval: isLive ? 3000 : false, + refetchInterval: detailRefetchInterval, }) const logFilters = useMemo( @@ -154,42 +212,73 @@ export default function Logs() { return { ...selectedLogFromList, ...activeLogQuery.data } }, [selectedLogFromList, activeLogQuery.data, isPreviewOpen]) + const handleLogHover = useCallback( + (log: WorkflowLog) => { + prefetchLogDetail(queryClient, log.id) + }, + [queryClient] + ) + useFolders(workspaceId) + useEffect(() => { + logsRef.current = logs + }, [logs]) + useEffect(() => { + selectedLogIndexRef.current = selectedLogIndex + }, [selectedLogIndex]) + useEffect(() => { + selectedLogIdRef.current = selectedLogId + }, [selectedLogId]) + useEffect(() => { + logsRefetchRef.current = logsQuery.refetch + }, [logsQuery.refetch]) + useEffect(() => { + activeLogRefetchRef.current = activeLogQuery.refetch + }, [activeLogQuery.refetch]) + useEffect(() => { + logsQueryRef.current = { + isFetching: logsQuery.isFetching, + hasNextPage: logsQuery.hasNextPage ?? false, + fetchNextPage: logsQuery.fetchNextPage, + } + }, [logsQuery.isFetching, logsQuery.hasNextPage, logsQuery.fetchNextPage]) + + useEffect(() => { + const timers = refreshTimersRef.current + return () => { + timers.forEach((id) => window.clearTimeout(id)) + timers.clear() + } + }, []) + useEffect(() => { if (isInitialized.current) { setStoreSearchQuery(debouncedSearchQuery) } }, [debouncedSearchQuery, setStoreSearchQuery]) - const handleLogClick = useCallback( - (log: WorkflowLog) => { - if (selectedLogId === log.id && isSidebarOpen) { - setIsSidebarOpen(false) - setSelectedLogId(null) - return - } - setSelectedLogId(log.id) - setIsSidebarOpen(true) - }, - [selectedLogId, isSidebarOpen] - ) + const handleLogClick = useCallback((log: WorkflowLog) => { + dispatch({ type: 'TOGGLE_LOG', logId: log.id }) + }, []) const handleNavigateNext = useCallback(() => { - if (selectedLogIndex < logs.length - 1) { - setSelectedLogId(logs[selectedLogIndex + 1].id) + const idx = selectedLogIndexRef.current + const currentLogs = logsRef.current + if (idx < currentLogs.length - 1) { + dispatch({ type: 'SELECT_LOG', logId: currentLogs[idx + 1].id }) } - }, [selectedLogIndex, logs]) + }, []) const handleNavigatePrev = useCallback(() => { - if (selectedLogIndex > 0) { - setSelectedLogId(logs[selectedLogIndex - 1].id) + const idx = selectedLogIndexRef.current + if (idx > 0) { + dispatch({ type: 'SELECT_LOG', logId: logsRef.current[idx - 1].id }) } - }, [selectedLogIndex, logs]) + }, []) const handleCloseSidebar = useCallback(() => { - setIsSidebarOpen(false) - setSelectedLogId(null) + dispatch({ type: 'CLOSE_SIDEBAR' }) }, []) const handleLogContextMenu = useCallback((e: React.MouseEvent, log: WorkflowLog) => { @@ -260,26 +349,34 @@ export default function Logs() { const handleRefresh = useCallback(() => { setIsVisuallyRefreshing(true) - setTimeout(() => setIsVisuallyRefreshing(false), REFRESH_SPINNER_DURATION_MS) - logsQuery.refetch() - if (selectedLogId) { - activeLogQuery.refetch() + const timerId = window.setTimeout(() => { + setIsVisuallyRefreshing(false) + refreshTimersRef.current.delete(timerId) + }, REFRESH_SPINNER_DURATION_MS) + refreshTimersRef.current.add(timerId) + logsRefetchRef.current() + if (selectedLogIdRef.current) { + activeLogRefetchRef.current() } - }, [logsQuery, activeLogQuery, selectedLogId]) + }, []) const handleToggleLive = useCallback(() => { - const newIsLive = !isLive - setIsLive(newIsLive) - - if (newIsLive) { - setIsVisuallyRefreshing(true) - setTimeout(() => setIsVisuallyRefreshing(false), REFRESH_SPINNER_DURATION_MS) - logsQuery.refetch() - if (selectedLogId) { - activeLogQuery.refetch() + setIsLive((prev) => { + if (!prev) { + setIsVisuallyRefreshing(true) + const timerId = window.setTimeout(() => { + setIsVisuallyRefreshing(false) + refreshTimersRef.current.delete(timerId) + }, REFRESH_SPINNER_DURATION_MS) + refreshTimersRef.current.add(timerId) + logsRefetchRef.current() + if (selectedLogIdRef.current) { + activeLogRefetchRef.current() + } } - } - }, [isLive, logsQuery, activeLogQuery, selectedLogId]) + return !prev + }) + }, []) const prevIsFetchingRef = useRef(logsQuery.isFetching) useEffect(() => { @@ -289,11 +386,15 @@ export default function Logs() { if (isLive && !wasFetching && isFetching) { setIsVisuallyRefreshing(true) - setTimeout(() => setIsVisuallyRefreshing(false), REFRESH_SPINNER_DURATION_MS) + const timerId = window.setTimeout(() => { + setIsVisuallyRefreshing(false) + refreshTimersRef.current.delete(timerId) + }, REFRESH_SPINNER_DURATION_MS) + refreshTimersRef.current.add(timerId) } }, [logsQuery.isFetching, isLive]) - const handleExport = async () => { + const handleExport = useCallback(async () => { setIsExporting(true) try { const params = new URLSearchParams() @@ -327,7 +428,17 @@ export default function Logs() { } finally { setIsExporting(false) } - } + }, [ + workspaceId, + level, + triggers, + workflowIds, + folderIds, + timeRange, + startDate, + endDate, + debouncedSearchQuery, + ]) useEffect(() => { if (!isInitialized.current) { @@ -348,41 +459,59 @@ export default function Logs() { }, [initializeFromURL]) const loadMoreLogs = useCallback(() => { - if (!logsQuery.isFetching && logsQuery.hasNextPage) { - logsQuery.fetchNextPage() + const { isFetching, hasNextPage, fetchNextPage } = logsQueryRef.current + if (!isFetching && hasNextPage) { + fetchNextPage() } - }, [logsQuery]) + }, []) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (isSearchOpenRef.current) return - if (logs.length === 0) return + const currentLogs = logsRef.current + const currentIndex = selectedLogIndexRef.current + if (currentLogs.length === 0) return - if (selectedLogIndex === -1 && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) { + if (currentIndex === -1 && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) { e.preventDefault() - setSelectedLogId(logs[0].id) + dispatch({ type: 'SELECT_LOG', logId: currentLogs[0].id }) return } - if (e.key === 'ArrowUp' && !e.metaKey && !e.ctrlKey && selectedLogIndex > 0) { + if (e.key === 'ArrowUp' && !e.metaKey && !e.ctrlKey && currentIndex > 0) { e.preventDefault() handleNavigatePrev() } - if (e.key === 'ArrowDown' && !e.metaKey && !e.ctrlKey && selectedLogIndex < logs.length - 1) { + if ( + e.key === 'ArrowDown' && + !e.metaKey && + !e.ctrlKey && + currentIndex < currentLogs.length - 1 + ) { e.preventDefault() handleNavigateNext() } - if (e.key === 'Enter' && selectedLogId) { + if (e.key === 'Enter' && selectedLogIdRef.current) { e.preventDefault() - setIsSidebarOpen(!isSidebarOpen) + dispatch({ type: 'TOGGLE_SIDEBAR' }) } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) - }, [logs, selectedLogIndex, isSidebarOpen, selectedLogId, handleNavigateNext, handleNavigatePrev]) + }, [handleNavigateNext, handleNavigatePrev]) + + const handleCloseContextMenu = useCallback(() => setContextMenuOpen(false), []) + const handleOpenNotificationSettings = useCallback(() => setIsNotificationSettingsOpen(true), []) + const handleSearchOpenChange = useCallback((open: boolean) => { + isSearchOpenRef.current = open + }, []) + const handleClosePreview = useCallback(() => { + setIsPreviewOpen(false) + setPreviewLogId(null) + }, []) const isDashboardView = viewMode === 'dashboard' @@ -402,12 +531,10 @@ export default function Logs() { onExport={handleExport} canEdit={userPermissions.canEdit} hasLogs={logs.length > 0} - onOpenNotificationSettings={() => setIsNotificationSettingsOpen(true)} + onOpenNotificationSettings={handleOpenNotificationSettings} searchQuery={searchQuery} onSearchQueryChange={setSearchQuery} - onSearchOpenChange={(open: boolean) => { - isSearchOpenRef.current = open - }} + onSearchOpenChange={handleSearchOpenChange} />
@@ -449,7 +576,7 @@ export default function Logs() { {/* Table body - virtualized */} -
+
{logsQuery.isLoading && !logsQuery.data ? (
@@ -476,6 +603,7 @@ export default function Logs() { logs={logs} selectedLogId={selectedLogId} onLogClick={handleLogClick} + onLogHover={handleLogHover} onLogContextMenu={handleLogContextMenu} selectedRowRef={selectedRowRef} hasNextPage={logsQuery.hasNextPage ?? false} @@ -511,7 +639,7 @@ export default function Logs() { isOpen={contextMenuOpen} position={contextMenuPosition} menuRef={contextMenuRef} - onClose={() => setContextMenuOpen(false)} + onClose={handleCloseContextMenu} log={contextMenuLog} onCopyExecutionId={handleCopyExecutionId} onOpenWorkflow={handleOpenWorkflow} @@ -528,10 +656,7 @@ export default function Logs() { traceSpans={activeLogQuery.data.executionData?.traceSpans} isModal isOpen={isPreviewOpen} - onClose={() => { - setIsPreviewOpen(false) - setPreviewLogId(null) - }} + onClose={handleClosePreview} /> )}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx index ea922b3baf..32036c21d8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx @@ -239,7 +239,12 @@ export const ComboBox = memo(function ComboBox({ */ const defaultOptionValue = useMemo(() => { if (defaultValue !== undefined) { - return defaultValue + // Validate that the default value exists in the available (filtered) options + const defaultInOptions = evaluatedOptions.find((opt) => getOptionValue(opt) === defaultValue) + if (defaultInOptions) { + return defaultValue + } + // Default not available (e.g. provider disabled) — fall through to other fallbacks } // For model field, default to claude-sonnet-4-5 if available diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx index 81ca1f03c5..c9444f3b36 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx @@ -1,9 +1,10 @@ 'use client' import { useEffect, useRef } from 'react' -import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block' import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { useSubBlockStore } from '@/stores/workflows/subblock/store' interface ToolSubBlockRendererProps { blockId: string @@ -44,53 +45,43 @@ export function ToolSubBlockRenderer({ canonicalToggle, }: ToolSubBlockRendererProps) { const syntheticId = `${subBlockId}-tool-${toolIndex}-${effectiveParamId}` - const [storeValue, setStoreValue] = useSubBlockValue(blockId, syntheticId) - const toolParamValue = toolParams?.[effectiveParamId] ?? '' const isObjectType = OBJECT_SUBBLOCK_TYPES.has(subBlock.type) - const lastPushedToStoreRef = useRef(null) - const lastPushedToParamsRef = useRef(null) + const syncedRef = useRef(null) + const onParamChangeRef = useRef(onParamChange) + onParamChangeRef.current = onParamChange useEffect(() => { - if (!toolParamValue && lastPushedToStoreRef.current === null) { - lastPushedToStoreRef.current = toolParamValue - lastPushedToParamsRef.current = toolParamValue - return - } - if (toolParamValue !== lastPushedToStoreRef.current) { - lastPushedToStoreRef.current = toolParamValue - lastPushedToParamsRef.current = toolParamValue - - if (isObjectType && typeof toolParamValue === 'string' && toolParamValue) { - try { - const parsed = JSON.parse(toolParamValue) - if (typeof parsed === 'object' && parsed !== null) { - setStoreValue(parsed) - return - } - } catch { - // Not valid JSON — fall through to set as string - } - } - setStoreValue(toolParamValue) - } - }, [toolParamValue, setStoreValue, isObjectType]) + const unsub = useSubBlockStore.subscribe((state, prevState) => { + const wfId = useWorkflowRegistry.getState().activeWorkflowId + if (!wfId) return + const newVal = state.workflowValues[wfId]?.[blockId]?.[syntheticId] + const oldVal = prevState.workflowValues[wfId]?.[blockId]?.[syntheticId] + if (newVal === oldVal) return + const stringified = + newVal == null ? '' : typeof newVal === 'string' ? newVal : JSON.stringify(newVal) + if (stringified === syncedRef.current) return + syncedRef.current = stringified + onParamChangeRef.current(toolIndex, effectiveParamId, stringified) + }) + return unsub + }, [blockId, syntheticId, toolIndex, effectiveParamId]) useEffect(() => { - if (storeValue == null && lastPushedToParamsRef.current === null) return - const stringValue = - storeValue == null - ? '' - : typeof storeValue === 'string' - ? storeValue - : JSON.stringify(storeValue) - if (stringValue !== lastPushedToParamsRef.current) { - lastPushedToParamsRef.current = stringValue - lastPushedToStoreRef.current = stringValue - onParamChange(toolIndex, effectiveParamId, stringValue) + if (toolParamValue === syncedRef.current) return + syncedRef.current = toolParamValue + if (isObjectType && toolParamValue) { + try { + const parsed = JSON.parse(toolParamValue) + if (typeof parsed === 'object' && parsed !== null) { + useSubBlockStore.getState().setValue(blockId, syntheticId, parsed) + return + } + } catch {} } - }, [storeValue, toolIndex, effectiveParamId, onParamChange]) + useSubBlockStore.getState().setValue(blockId, syntheticId, toolParamValue) + }, [toolParamValue, blockId, syntheticId, isObjectType]) const visibility = subBlock.paramVisibility ?? 'user-or-llm' const isOptionalForUser = visibility !== 'user-only' 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 f92b8150af..e8fe08e5be 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 @@ -1741,36 +1741,97 @@ export const ToolInput = memo(function ToolInput({ ) : null })()} - {requiresOAuth && oauthConfig && ( -
-
- Account * -
-
- - handleParamChange(toolIndex, 'credential', value) - } - provider={oauthConfig.provider as OAuthProvider} - requiredScopes={ - toolBlock?.subBlocks?.find((sb) => sb.id === 'credential') - ?.requiredScopes || - getCanonicalScopesForProvider(oauthConfig.provider) - } - serviceId={oauthConfig.provider} - disabled={disabled} - /> -
-
- )} - {(() => { const renderedElements: React.ReactNode[] = [] + const showOAuth = + requiresOAuth && oauthConfig && tool.params?.authMethod !== 'bot_token' + + const renderOAuthAccount = (): React.ReactNode => { + if (!showOAuth || !oauthConfig) return null + const credentialSubBlock = toolBlock?.subBlocks?.find( + (s) => s.type === 'oauth-input' + ) + return ( +
+
+ {credentialSubBlock?.title || 'Account'}{' '} + * +
+
+ + handleParamChange(toolIndex, 'credential', value) + } + provider={oauthConfig.provider as OAuthProvider} + requiredScopes={ + credentialSubBlock?.requiredScopes || + getCanonicalScopesForProvider(oauthConfig.provider) + } + serviceId={oauthConfig.provider} + disabled={disabled} + /> +
+
+ ) + } + + const renderSubBlock = (sb: BlockSubBlockConfig): React.ReactNode => { + const effectiveParamId = sb.id + const canonicalId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id] + const canonicalGroup = canonicalId + ? toolCanonicalIndex?.groupsById[canonicalId] + : undefined + const hasCanonicalPair = isCanonicalPair(canonicalGroup) + const canonicalMode = + canonicalGroup && hasCanonicalPair + ? resolveCanonicalMode( + canonicalGroup, + { operation: tool.operation, ...tool.params }, + toolScopedOverrides + ) + : undefined + + const canonicalToggleProp = + hasCanonicalPair && canonicalMode && canonicalId + ? { + mode: canonicalMode, + onToggle: () => { + const nextMode = canonicalMode === 'advanced' ? 'basic' : 'advanced' + collaborativeSetBlockCanonicalMode( + blockId, + `${tool.type}:${canonicalId}`, + nextMode + ) + }, + } + : undefined + + const sbWithTitle = sb.title + ? sb + : { ...sb, title: formatParameterLabel(effectiveParamId) } + + return ( + + ) + } + if (useSubBlocks && displaySubBlocks.length > 0) { + const allBlockSubBlocks = toolBlock?.subBlocks || [] const coveredParamIds = new Set( - displaySubBlocks.flatMap((sb) => { + allBlockSubBlocks.flatMap((sb) => { const ids = [sb.id] if (sb.canonicalParamId) ids.push(sb.canonicalParamId) const cId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id] @@ -1785,57 +1846,45 @@ export const ToolInput = memo(function ToolInput({ }) ) - displaySubBlocks.forEach((sb) => { - const effectiveParamId = sb.id - const canonicalId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id] - const canonicalGroup = canonicalId - ? toolCanonicalIndex?.groupsById[canonicalId] - : undefined - const hasCanonicalPair = isCanonicalPair(canonicalGroup) - const canonicalMode = - canonicalGroup && hasCanonicalPair - ? resolveCanonicalMode( - canonicalGroup, - { operation: tool.operation, ...tool.params }, - toolScopedOverrides - ) - : undefined - - const canonicalToggleProp = - hasCanonicalPair && canonicalMode && canonicalId - ? { - mode: canonicalMode, - onToggle: () => { - const nextMode = - canonicalMode === 'advanced' ? 'basic' : 'advanced' - collaborativeSetBlockCanonicalMode( - blockId, - `${tool.type}:${canonicalId}`, - nextMode - ) - }, - } - : undefined + type RenderItem = + | { kind: 'subblock'; sb: BlockSubBlockConfig } + | { kind: 'oauth' } - const sbWithTitle = sb.title - ? sb - : { ...sb, title: formatParameterLabel(effectiveParamId) } + const renderOrder: RenderItem[] = displaySubBlocks.map((sb) => ({ + kind: 'subblock' as const, + sb, + })) - renderedElements.push( - + if (showOAuth) { + const credentialIdx = allBlockSubBlocks.findIndex( + (sb) => sb.type === 'oauth-input' ) - }) + if (credentialIdx >= 0) { + const sbPositions = new Map(allBlockSubBlocks.map((sb, i) => [sb.id, i])) + const insertAt = renderOrder.findIndex( + (item) => + item.kind === 'subblock' && + (sbPositions.get(item.sb.id) ?? Number.POSITIVE_INFINITY) > + credentialIdx + ) + if (insertAt === -1) { + renderOrder.push({ kind: 'oauth' }) + } else { + renderOrder.splice(insertAt, 0, { kind: 'oauth' }) + } + } else { + renderOrder.unshift({ kind: 'oauth' }) + } + } + + for (const item of renderOrder) { + if (item.kind === 'oauth') { + const el = renderOAuthAccount() + if (el) renderedElements.push(el) + } else { + renderedElements.push(renderSubBlock(item.sb)) + } + } const uncoveredParams = displayParams.filter( (param) => @@ -1873,6 +1922,11 @@ export const ToolInput = memo(function ToolInput({ ) } + { + const el = renderOAuthAccount() + if (el) renderedElements.push(el) + } + const filteredParams = displayParams.filter((param) => evaluateParameterCondition(param, tool) ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/block.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/block.tsx index 07b8839dd6..a55934a0e2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/block.tsx @@ -223,7 +223,12 @@ function resolveToolsDisplay( * - Resolves tool names from block registry * - Shows '-' for other selector types that need hydration */ -function SubBlockRow({ title, value, subBlock, rawValue }: SubBlockRowProps) { +const SubBlockRow = memo(function SubBlockRow({ + title, + value, + subBlock, + rawValue, +}: SubBlockRowProps) { const isPasswordField = subBlock?.password === true const maskedValue = isPasswordField && value && value !== '-' ? '•••' : null @@ -255,7 +260,7 @@ function SubBlockRow({ title, value, subBlock, rawValue }: SubBlockRowProps) { )}
) -} +}) /** * Preview block component for workflow visualization. diff --git a/apps/sim/blocks/blocks/agent.ts b/apps/sim/blocks/blocks/agent.ts index 84355f92b8..a5cf7f887e 100644 --- a/apps/sim/blocks/blocks/agent.ts +++ b/apps/sim/blocks/blocks/agent.ts @@ -2,11 +2,10 @@ import { createLogger } from '@sim/logger' import { AgentIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' import { AuthMode } from '@/blocks/types' -import { getApiKeyCondition } from '@/blocks/utils' +import { getApiKeyCondition, getModelOptions } from '@/blocks/utils' import { getBaseModelProviders, getMaxTemperature, - getProviderIcon, getReasoningEffortValuesForModel, getThinkingLevelsForModel, getVerbosityValuesForModel, @@ -18,7 +17,6 @@ import { providers, supportsTemperature, } from '@/providers/utils' -import { useProvidersStore } from '@/stores/providers' import type { ToolResponse } from '@/tools/types' const logger = createLogger('AgentBlock') @@ -121,21 +119,7 @@ Return ONLY the JSON array.`, placeholder: 'Type or select a model...', required: true, defaultValue: 'claude-sonnet-4-5', - options: () => { - const providersState = useProvidersStore.getState() - const baseModels = providersState.providers.base.models - const ollamaModels = providersState.providers.ollama.models - const vllmModels = providersState.providers.vllm.models - const openrouterModels = providersState.providers.openrouter.models - const allModels = Array.from( - new Set([...baseModels, ...ollamaModels, ...vllmModels, ...openrouterModels]) - ) - - return allModels.map((model) => { - const icon = getProviderIcon(model) - return { label: model, id: model, ...(icon && { icon }) } - }) - }, + options: getModelOptions, }, { id: 'vertexCredential', diff --git a/apps/sim/blocks/blocks/evaluator.ts b/apps/sim/blocks/blocks/evaluator.ts index 5d584d171e..4edb032ba9 100644 --- a/apps/sim/blocks/blocks/evaluator.ts +++ b/apps/sim/blocks/blocks/evaluator.ts @@ -1,10 +1,13 @@ import { createLogger } from '@sim/logger' import { ChartBarIcon } from '@/components/icons' import type { BlockConfig, ParamType } from '@/blocks/types' -import { getProviderCredentialSubBlocks, PROVIDER_CREDENTIAL_INPUTS } from '@/blocks/utils' +import { + getModelOptions, + getProviderCredentialSubBlocks, + PROVIDER_CREDENTIAL_INPUTS, +} from '@/blocks/utils' import type { ProviderId } from '@/providers/types' -import { getBaseModelProviders, getProviderIcon } from '@/providers/utils' -import { useProvidersStore } from '@/stores/providers/store' +import { getBaseModelProviders } from '@/providers/utils' import type { ToolResponse } from '@/tools/types' const logger = createLogger('EvaluatorBlock') @@ -175,21 +178,7 @@ export const EvaluatorBlock: BlockConfig = { placeholder: 'Type or select a model...', required: true, defaultValue: 'claude-sonnet-4-5', - options: () => { - const providersState = useProvidersStore.getState() - const baseModels = providersState.providers.base.models - const ollamaModels = providersState.providers.ollama.models - const vllmModels = providersState.providers.vllm.models - const openrouterModels = providersState.providers.openrouter.models - const allModels = Array.from( - new Set([...baseModels, ...ollamaModels, ...vllmModels, ...openrouterModels]) - ) - - return allModels.map((model) => { - const icon = getProviderIcon(model) - return { label: model, id: model, ...(icon && { icon }) } - }) - }, + options: getModelOptions, }, ...getProviderCredentialSubBlocks(), { diff --git a/apps/sim/blocks/blocks/guardrails.ts b/apps/sim/blocks/blocks/guardrails.ts index 4ccf1ccecf..5eeacd9a63 100644 --- a/apps/sim/blocks/blocks/guardrails.ts +++ b/apps/sim/blocks/blocks/guardrails.ts @@ -1,8 +1,10 @@ import { ShieldCheckIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' -import { getProviderCredentialSubBlocks, PROVIDER_CREDENTIAL_INPUTS } from '@/blocks/utils' -import { getProviderIcon } from '@/providers/utils' -import { useProvidersStore } from '@/stores/providers/store' +import { + getModelOptions, + getProviderCredentialSubBlocks, + PROVIDER_CREDENTIAL_INPUTS, +} from '@/blocks/utils' import type { ToolResponse } from '@/tools/types' export interface GuardrailsResponse extends ToolResponse { @@ -111,21 +113,7 @@ Return ONLY the regex pattern - no explanations, no quotes, no forward slashes, type: 'combobox', placeholder: 'Type or select a model...', required: true, - options: () => { - const providersState = useProvidersStore.getState() - const baseModels = providersState.providers.base.models - const ollamaModels = providersState.providers.ollama.models - const vllmModels = providersState.providers.vllm.models - const openrouterModels = providersState.providers.openrouter.models - const allModels = Array.from( - new Set([...baseModels, ...ollamaModels, ...vllmModels, ...openrouterModels]) - ) - - return allModels.map((model) => { - const icon = getProviderIcon(model) - return { label: model, id: model, ...(icon && { icon }) } - }) - }, + options: getModelOptions, condition: { field: 'validationType', value: ['hallucination'], diff --git a/apps/sim/blocks/blocks/router.ts b/apps/sim/blocks/blocks/router.ts index 7da50ed98d..4fae2383ff 100644 --- a/apps/sim/blocks/blocks/router.ts +++ b/apps/sim/blocks/blocks/router.ts @@ -1,9 +1,12 @@ import { ConnectIcon } from '@/components/icons' import { AuthMode, type BlockConfig } from '@/blocks/types' -import { getProviderCredentialSubBlocks, PROVIDER_CREDENTIAL_INPUTS } from '@/blocks/utils' +import { + getModelOptions, + getProviderCredentialSubBlocks, + PROVIDER_CREDENTIAL_INPUTS, +} from '@/blocks/utils' import type { ProviderId } from '@/providers/types' -import { getBaseModelProviders, getProviderIcon } from '@/providers/utils' -import { useProvidersStore } from '@/stores/providers' +import { getBaseModelProviders } from '@/providers/utils' import type { ToolResponse } from '@/tools/types' interface RouterResponse extends ToolResponse { @@ -134,25 +137,6 @@ Respond with a JSON object containing: - reasoning: A brief explanation (1-2 sentences) of why you chose this route` } -/** - * Helper to get model options for both router versions. - */ -const getModelOptions = () => { - const providersState = useProvidersStore.getState() - const baseModels = providersState.providers.base.models - const ollamaModels = providersState.providers.ollama.models - const vllmModels = providersState.providers.vllm.models - const openrouterModels = providersState.providers.openrouter.models - const allModels = Array.from( - new Set([...baseModels, ...ollamaModels, ...vllmModels, ...openrouterModels]) - ) - - return allModels.map((model) => { - const icon = getProviderIcon(model) - return { label: model, id: model, ...(icon && { icon }) } - }) -} - /** * Legacy Router Block (block-based routing). * Hidden from toolbar but still supported for existing workflows. diff --git a/apps/sim/blocks/blocks/schedule.ts b/apps/sim/blocks/blocks/schedule.ts index fb757543e3..0757eca0e7 100644 --- a/apps/sim/blocks/blocks/schedule.ts +++ b/apps/sim/blocks/blocks/schedule.ts @@ -122,6 +122,25 @@ export const ScheduleBlock: BlockConfig = { required: true, mode: 'trigger', condition: { field: 'scheduleType', value: 'custom' }, + wandConfig: { + enabled: true, + prompt: `You are an expert at writing cron expressions. Generate a valid cron expression based on the user's description. + +Cron format: minute hour day-of-month month day-of-week +- minute: 0-59 +- hour: 0-23 +- day-of-month: 1-31 +- month: 1-12 +- day-of-week: 0-7 (0 and 7 are Sunday) + +Special characters: * (any), , (list), - (range), / (step) + +{context} + +Return ONLY the cron expression, nothing else. No explanation, no backticks, no quotes.`, + placeholder: 'Describe your schedule (e.g., "every weekday at 9am")', + generationType: 'cron-expression', + }, }, { diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index 38f22ca78c..c4337fab4c 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -604,7 +604,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, case 'send': { baseParams.text = text if (threadTs) { - baseParams.thread_ts = threadTs + baseParams.threadTs = threadTs } // files is the canonical param from attachmentFiles (basic) or files (advanced) const normalizedFiles = normalizeFileInput(files) diff --git a/apps/sim/blocks/blocks/translate.ts b/apps/sim/blocks/blocks/translate.ts index 1385075c73..a47a2e06ef 100644 --- a/apps/sim/blocks/blocks/translate.ts +++ b/apps/sim/blocks/blocks/translate.ts @@ -1,8 +1,10 @@ import { TranslateIcon } from '@/components/icons' import { AuthMode, type BlockConfig } from '@/blocks/types' -import { getProviderCredentialSubBlocks, PROVIDER_CREDENTIAL_INPUTS } from '@/blocks/utils' -import { getProviderIcon } from '@/providers/utils' -import { useProvidersStore } from '@/stores/providers/store' +import { + getModelOptions, + getProviderCredentialSubBlocks, + PROVIDER_CREDENTIAL_INPUTS, +} from '@/blocks/utils' const getTranslationPrompt = (targetLanguage: string) => `Translate the following text into ${targetLanguage || 'English'}. Output ONLY the translated text with no additional commentary, explanations, or notes.` @@ -38,18 +40,7 @@ export const TranslateBlock: BlockConfig = { type: 'combobox', placeholder: 'Type or select a model...', required: true, - options: () => { - const providersState = useProvidersStore.getState() - const baseModels = providersState.providers.base.models - const ollamaModels = providersState.providers.ollama.models - const openrouterModels = providersState.providers.openrouter.models - const allModels = Array.from(new Set([...baseModels, ...ollamaModels, ...openrouterModels])) - - return allModels.map((model) => { - const icon = getProviderIcon(model) - return { label: model, id: model, ...(icon && { icon }) } - }) - }, + options: getModelOptions, }, ...getProviderCredentialSubBlocks(), { diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 8ac262bef5..30158d7347 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -40,6 +40,7 @@ export type GenerationType = | 'neo4j-parameters' | 'timestamp' | 'timezone' + | 'cron-expression' export type SubBlockType = | 'short-input' // Single line input diff --git a/apps/sim/blocks/utils.ts b/apps/sim/blocks/utils.ts index 8c003e0ada..32ab701bb4 100644 --- a/apps/sim/blocks/utils.ts +++ b/apps/sim/blocks/utils.ts @@ -1,8 +1,32 @@ import { isHosted } from '@/lib/core/config/feature-flags' import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types' -import { getHostedModels, getProviderFromModel, providers } from '@/providers/utils' +import { + getHostedModels, + getProviderFromModel, + getProviderIcon, + providers, +} from '@/providers/utils' import { useProvidersStore } from '@/stores/providers/store' +/** + * Returns model options for combobox subblocks, combining all provider sources. + */ +export function getModelOptions() { + const providersState = useProvidersStore.getState() + const baseModels = providersState.providers.base.models + const ollamaModels = providersState.providers.ollama.models + const vllmModels = providersState.providers.vllm.models + const openrouterModels = providersState.providers.openrouter.models + const allModels = Array.from( + new Set([...baseModels, ...ollamaModels, ...vllmModels, ...openrouterModels]) + ) + + return allModels.map((model) => { + const icon = getProviderIcon(model) + return { label: model, id: model, ...(icon && { icon }) } + }) +} + /** * Checks if a field is included in the dependsOn config. * Handles both simple array format and object format with all/any fields. diff --git a/apps/sim/components/emcn/components/code/code.tsx b/apps/sim/components/emcn/components/code/code.tsx index e0a40846bf..a22f56e927 100644 --- a/apps/sim/components/emcn/components/code/code.tsx +++ b/apps/sim/components/emcn/components/code/code.tsx @@ -1008,7 +1008,7 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({ * Non-virtualized code viewer implementation. * Renders all lines directly without windowing. */ -function ViewerInner({ +const ViewerInner = memo(function ViewerInner({ code, showGutter, language, @@ -1181,7 +1181,7 @@ function ViewerInner({ ) -} +}) /** * Readonly code viewer with optional gutter and syntax highlighting. diff --git a/apps/sim/hooks/queries/logs.ts b/apps/sim/hooks/queries/logs.ts index 39f0cffe6e..3b8e71480c 100644 --- a/apps/sim/hooks/queries/logs.ts +++ b/apps/sim/hooks/queries/logs.ts @@ -1,4 +1,10 @@ -import { keepPreviousData, useInfiniteQuery, useQuery } from '@tanstack/react-query' +import { + keepPreviousData, + type QueryClient, + useInfiniteQuery, + useQuery, + useQueryClient, +} from '@tanstack/react-query' import { getEndDateFromTimeRange, getStartDateFromTimeRange } from '@/lib/logs/filters' import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser' import type { @@ -146,17 +152,45 @@ export function useLogsList( interface UseLogDetailOptions { enabled?: boolean - refetchInterval?: number | false + refetchInterval?: + | number + | false + | ((query: { state: { data?: WorkflowLog } }) => number | false | undefined) } export function useLogDetail(logId: string | undefined, options?: UseLogDetailOptions) { + const queryClient = useQueryClient() return useQuery({ queryKey: logKeys.detail(logId), queryFn: () => fetchLogDetail(logId as string), enabled: Boolean(logId) && (options?.enabled ?? true), refetchInterval: options?.refetchInterval ?? false, staleTime: 30 * 1000, - placeholderData: keepPreviousData, + initialData: () => { + if (!logId) return undefined + const listQueries = queryClient.getQueriesData<{ + pages: { logs: WorkflowLog[] }[] + }>({ + queryKey: logKeys.lists(), + }) + for (const [, data] of listQueries) { + const match = data?.pages?.flatMap((p) => p.logs).find((l) => l.id === logId) + if (match) return match + } + return undefined + }, + initialDataUpdatedAt: 0, + }) +} + +/** + * Prefetches log detail data on hover for instant panel rendering on click. + */ +export function prefetchLogDetail(queryClient: QueryClient, logId: string) { + queryClient.prefetchQuery({ + queryKey: logKeys.detail(logId), + queryFn: () => fetchLogDetail(logId), + staleTime: 30 * 1000, }) } diff --git a/apps/sim/lib/workflows/operations/import-export.ts b/apps/sim/lib/workflows/operations/import-export.ts index fb741f7c44..bf211bdcdf 100644 --- a/apps/sim/lib/workflows/operations/import-export.ts +++ b/apps/sim/lib/workflows/operations/import-export.ts @@ -583,7 +583,10 @@ export function parseWorkflowJson( loops: workflowData.loops || {}, parallels: workflowData.parallels || {}, metadata: workflowData.metadata, - variables: Array.isArray(workflowData.variables) ? workflowData.variables : undefined, + variables: + workflowData.variables && typeof workflowData.variables === 'object' + ? workflowData.variables + : undefined, } if (regenerateIdsFlag) { diff --git a/apps/sim/lib/workflows/persistence/duplicate.ts b/apps/sim/lib/workflows/persistence/duplicate.ts index 817f58c9c8..329d077e95 100644 --- a/apps/sim/lib/workflows/persistence/duplicate.ts +++ b/apps/sim/lib/workflows/persistence/duplicate.ts @@ -33,6 +33,44 @@ interface DuplicateWorkflowResult { subflowsCount: number } +/** + * Remaps old variable IDs to new variable IDs inside block subBlocks. + * Specifically targets `variables-input` subblocks whose value is an array + * of variable assignments containing a `variableId` field. + */ +function remapVariableIdsInSubBlocks( + subBlocks: Record, + varIdMap: Map +): Record { + const updated: Record = {} + + for (const [key, subBlock] of Object.entries(subBlocks)) { + if ( + subBlock && + typeof subBlock === 'object' && + subBlock.type === 'variables-input' && + Array.isArray(subBlock.value) + ) { + updated[key] = { + ...subBlock, + value: subBlock.value.map((assignment: any) => { + if (assignment && typeof assignment === 'object' && assignment.variableId) { + const newVarId = varIdMap.get(assignment.variableId) + if (newVarId) { + return { ...assignment, variableId: newVarId } + } + } + return assignment + }), + } + } else { + updated[key] = subBlock + } + } + + return updated +} + /** * Duplicate a workflow with all its blocks, edges, and subflows * This is a shared helper used by both the workflow duplicate API and folder duplicate API @@ -104,6 +142,9 @@ export async function duplicateWorkflow( .where(and(eq(workflow.workspaceId, targetWorkspaceId), folderCondition)) const sortOrder = (minResult?.minOrder ?? 1) - 1 + // Mapping from old variable IDs to new variable IDs (populated during variable duplication) + const varIdMapping = new Map() + // Create the new workflow first (required for foreign key constraints) await tx.insert(workflow).values({ id: newWorkflowId, @@ -123,8 +164,9 @@ export async function duplicateWorkflow( variables: (() => { const sourceVars = (source.variables as Record) || {} const remapped: Record = {} - for (const [, variable] of Object.entries(sourceVars) as [string, Variable][]) { + for (const [oldVarId, variable] of Object.entries(sourceVars) as [string, Variable][]) { const newVarId = crypto.randomUUID() + varIdMapping.set(oldVarId, newVarId) remapped[newVarId] = { ...variable, id: newVarId, @@ -181,6 +223,20 @@ export async function duplicateWorkflow( } } + // Update variable references in subBlocks (e.g. variables-input assignments) + let updatedSubBlocks = block.subBlocks + if ( + varIdMapping.size > 0 && + block.subBlocks && + typeof block.subBlocks === 'object' && + !Array.isArray(block.subBlocks) + ) { + updatedSubBlocks = remapVariableIdsInSubBlocks( + block.subBlocks as Record, + varIdMapping + ) + } + return { ...block, id: newBlockId, @@ -188,6 +244,7 @@ export async function duplicateWorkflow( parentId: newParentId, extent: newExtent, data: updatedData, + subBlocks: updatedSubBlocks, locked: false, // Duplicated blocks should always be unlocked createdAt: now, updatedAt: now, diff --git a/apps/sim/lib/workflows/sanitization/json-sanitizer.ts b/apps/sim/lib/workflows/sanitization/json-sanitizer.ts index 0e452ccd1d..7afec494ce 100644 --- a/apps/sim/lib/workflows/sanitization/json-sanitizer.ts +++ b/apps/sim/lib/workflows/sanitization/json-sanitizer.ts @@ -57,12 +57,15 @@ export interface ExportWorkflowState { sortOrder?: number exportedAt?: string } - variables?: Array<{ - id: string - name: string - type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'plain' - value: unknown - }> + variables?: Record< + string, + { + id: string + name: string + type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'plain' + value: unknown + } + > } } diff --git a/apps/sim/providers/vertex/index.ts b/apps/sim/providers/vertex/index.ts index 6c1e5b1c95..145de12388 100644 --- a/apps/sim/providers/vertex/index.ts +++ b/apps/sim/providers/vertex/index.ts @@ -30,8 +30,8 @@ export const vertexProvider: ProviderConfig = { executeRequest: async ( request: ProviderRequest ): Promise => { - const vertexProject = env.VERTEX_PROJECT || request.vertexProject - const vertexLocation = env.VERTEX_LOCATION || request.vertexLocation || 'us-central1' + const vertexProject = request.vertexProject || env.VERTEX_PROJECT + const vertexLocation = request.vertexLocation || env.VERTEX_LOCATION || 'us-central1' if (!vertexProject) { throw new Error( diff --git a/apps/sim/tools/params.ts b/apps/sim/tools/params.ts index 89a9d0f8dc..66bbdc2926 100644 --- a/apps/sim/tools/params.ts +++ b/apps/sim/tools/params.ts @@ -827,11 +827,10 @@ export function formatParameterLabel(paramId: string): string { } /** - * SubBlock IDs that are "structural" — they control tool routing or auth, - * not user-facing parameters. These are excluded from tool-input rendering - * unless they have an explicit paramVisibility set. + * SubBlock IDs that control tool routing, not user-facing parameters. + * Excluded from tool-input rendering unless they have an explicit paramVisibility set. */ -const STRUCTURAL_SUBBLOCK_IDS = new Set(['operation', 'authMethod', 'destinationType']) +const STRUCTURAL_SUBBLOCK_IDS = new Set(['operation']) /** * SubBlock types that represent auth/credential inputs handled separately @@ -955,12 +954,8 @@ export function getSubBlocksForToolInput( } else if (sb.id in toolParamVisibility) { visibility = toolParamVisibility[sb.id] } else if (sb.canonicalParamId) { - // SubBlock has a canonicalParamId that doesn't directly match a tool param. - // This means the block's params() function transforms it before sending to the tool - // (e.g. listFolderId → folderId). These are user-facing inputs, default to user-or-llm. visibility = 'user-or-llm' } else { - // SubBlock has no corresponding tool param — skip it continue } } diff --git a/apps/sim/tools/slack/message.ts b/apps/sim/tools/slack/message.ts index 1fd60b6850..b1f0474033 100644 --- a/apps/sim/tools/slack/message.ts +++ b/apps/sim/tools/slack/message.ts @@ -57,7 +57,7 @@ export const slackMessageTool: ToolConfig