From d75ea37b3ca3203032eae65c5257e48f93ed8cd8 Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 16 Jan 2026 18:18:40 -0800 Subject: [PATCH 1/5] chore(readme): updated readme (#2861) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f25393ba18..e93ac2a4bb 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,12 @@

Sim.ai Discord - Twitter - Documentation DeepWiki + Twitter + Documentation

- Set Up with Cursor + Ask DeepWiki Set Up with Cursor

### Build Workflows with Ease From d024c1e4891b8a1b11033582eec162a318cce851 Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 16 Jan 2026 19:52:51 -0800 Subject: [PATCH 2/5] fix(shift): fix shift select blue ring fading (#2863) --- .../components/note-block/note-block.tsx | 7 ++++- .../components/subflows/subflow-node.tsx | 12 ++++---- .../workflow-block/workflow-block.tsx | 4 +-- .../w/[workflowId]/hooks/use-block-visual.ts | 12 ++++++-- .../w/[workflowId]/utils/block-ring-utils.ts | 28 +++++++++++++++---- 5 files changed, 48 insertions(+), 15 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx index 31c64f908a..2cfa568165 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx @@ -168,12 +168,17 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string } ) }) -export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps) { +export const NoteBlock = memo(function NoteBlock({ + id, + data, + selected, +}: NodeProps) { const { type, config, name } = data const { activeWorkflowId, isEnabled, handleClick, hasRing, ringStyles } = useBlockVisual({ blockId: id, data, + isSelected: selected, }) const storedValues = useSubBlockStore( useCallback( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx index 04e64907fa..3733301c44 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx @@ -66,7 +66,7 @@ export interface SubflowNodeData { * @param props - Node properties containing data and id * @returns Rendered subflow node component */ -export const SubflowNodeComponent = memo(({ data, id }: NodeProps) => { +export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps) => { const { getNodes } = useReactFlow() const blockRef = useRef(null) const userPermissions = useUserPermissionsContext() @@ -134,13 +134,15 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps {!isPreview && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 4559467522..8c8897fbd3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -208,7 +208,6 @@ const tryParseJson = (value: unknown): unknown => { export const getDisplayValue = (value: unknown): string => { if (value == null || value === '') return '-' - // Try parsing JSON strings first const parsedValue = tryParseJson(value) if (isMessagesArray(parsedValue)) { @@ -557,6 +556,7 @@ const SubBlockRow = ({ export const WorkflowBlock = memo(function WorkflowBlock({ id, data, + selected, }: NodeProps) { const { type, config, name, isPending } = data @@ -574,7 +574,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({ hasRing, ringStyles, runPathStatus, - } = useBlockVisual({ blockId: id, data, isPending }) + } = useBlockVisual({ blockId: id, data, isPending, isSelected: selected }) const currentBlock = currentWorkflow.getBlockById(id) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-visual.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-visual.ts index 20246d97d2..33f83bdb94 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-visual.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-visual.ts @@ -17,6 +17,8 @@ interface UseBlockVisualProps { data: WorkflowBlockProps /** Whether the block is pending execution */ isPending?: boolean + /** Whether the block is selected (via shift-click or selection box) */ + isSelected?: boolean } /** @@ -28,7 +30,12 @@ interface UseBlockVisualProps { * @param props - The hook properties * @returns Visual state, click handler, and ring styling for the block */ -export function useBlockVisual({ blockId, data, isPending = false }: UseBlockVisualProps) { +export function useBlockVisual({ + blockId, + data, + isPending = false, + isSelected = false, +}: UseBlockVisualProps) { const isPreview = data.isPreview ?? false const isPreviewSelected = data.isPreviewSelected ?? false @@ -42,7 +49,6 @@ export function useBlockVisual({ blockId, data, isPending = false }: UseBlockVis isDeletedBlock, } = useBlockState(blockId, currentWorkflow, data) - // Check if the editor panel is open for this block const currentBlockId = usePanelEditorStore((state) => state.currentBlockId) const activeTab = usePanelStore((state) => state.activeTab) const isEditorOpen = !isPreview && currentBlockId === blockId && activeTab === 'editor' @@ -68,6 +74,7 @@ export function useBlockVisual({ blockId, data, isPending = false }: UseBlockVis diffStatus: isPreview ? undefined : diffStatus, runPathStatus, isPreviewSelection: isPreview && isPreviewSelected, + isSelected: isPreview ? false : isSelected, }), [ isExecuting, @@ -78,6 +85,7 @@ export function useBlockVisual({ blockId, data, isPending = false }: UseBlockVis runPathStatus, isPreview, isPreviewSelected, + isSelected, ] ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-ring-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-ring-utils.ts index 19eec88625..62c178337b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-ring-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-ring-utils.ts @@ -14,6 +14,8 @@ export interface BlockRingOptions { diffStatus: BlockDiffStatus runPathStatus: BlockRunPathStatus isPreviewSelection?: boolean + /** Whether the block is selected via shift-click or selection box (shows blue ring) */ + isSelected?: boolean } /** @@ -32,11 +34,13 @@ export function getBlockRingStyles(options: BlockRingOptions): { diffStatus, runPathStatus, isPreviewSelection, + isSelected, } = options const hasRing = isExecuting || isEditorOpen || + isSelected || isPending || diffStatus === 'new' || diffStatus === 'edited' || @@ -46,25 +50,37 @@ export function getBlockRingStyles(options: BlockRingOptions): { const ringClassName = cn( // Executing block: pulsing success ring with prominent thickness (highest priority) isExecuting && 'ring-[3.5px] ring-[var(--border-success)] animate-ring-pulse', - // Editor open or preview selection: static blue ring + // Editor open, selected, or preview selection: static blue ring !isExecuting && - (isEditorOpen || isPreviewSelection) && + (isEditorOpen || isSelected || isPreviewSelection) && 'ring-[1.75px] ring-[var(--brand-secondary)]', // Non-active states use standard ring utilities - !isExecuting && !isEditorOpen && !isPreviewSelection && hasRing && 'ring-[1.75px]', + !isExecuting && + !isEditorOpen && + !isSelected && + !isPreviewSelection && + hasRing && + 'ring-[1.75px]', // Pending state: warning ring - !isExecuting && !isEditorOpen && isPending && 'ring-[var(--warning)]', + !isExecuting && !isEditorOpen && !isSelected && isPending && 'ring-[var(--warning)]', // Deleted state (highest priority after active/pending) - !isExecuting && !isEditorOpen && !isPending && isDeletedBlock && 'ring-[var(--text-error)]', + !isExecuting && + !isEditorOpen && + !isSelected && + !isPending && + isDeletedBlock && + 'ring-[var(--text-error)]', // Diff states !isExecuting && !isEditorOpen && + !isSelected && !isPending && !isDeletedBlock && diffStatus === 'new' && 'ring-[var(--brand-tertiary-2)]', !isExecuting && !isEditorOpen && + !isSelected && !isPending && !isDeletedBlock && diffStatus === 'edited' && @@ -72,6 +88,7 @@ export function getBlockRingStyles(options: BlockRingOptions): { // Run path states (lowest priority - only show if no other states active) !isExecuting && !isEditorOpen && + !isSelected && !isPending && !isDeletedBlock && !diffStatus && @@ -79,6 +96,7 @@ export function getBlockRingStyles(options: BlockRingOptions): { 'ring-[var(--border-success)]', !isExecuting && !isEditorOpen && + !isSelected && !isPending && !isDeletedBlock && !diffStatus && From b14672887be66c90ded16b47db81b7e2bc102c41 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 16 Jan 2026 19:53:14 -0800 Subject: [PATCH 3/5] fix(sockets): webhooks logic removal from copilot ops (#2862) * fix(sockets): dying on deployed webhooks * fix edit workflow --- .../tools/server/workflow/edit-workflow.ts | 4 +- apps/sim/lib/workflows/persistence/utils.ts | 50 ------------------- apps/sim/socket/database/operations.ts | 35 ------------- 3 files changed, 3 insertions(+), 86 deletions(-) diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts index a17af3cd3a..fb1598fc20 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -2499,7 +2499,9 @@ export const editWorkflowServerTool: BaseServerTool = { async execute(params: EditWorkflowParams, context?: { userId: string }): Promise { const logger = createLogger('EditWorkflowServerTool') const { operations, workflowId, currentUserWorkflow } = params - if (!operations || operations.length === 0) throw new Error('operations are required') + if (!Array.isArray(operations) || operations.length === 0) { + throw new Error('operations are required and must be an array') + } if (!workflowId) throw new Error('workflowId is required') logger.info('Executing edit_workflow', { diff --git a/apps/sim/lib/workflows/persistence/utils.ts b/apps/sim/lib/workflows/persistence/utils.ts index c27fefcf6a..d3b26e4ea6 100644 --- a/apps/sim/lib/workflows/persistence/utils.ts +++ b/apps/sim/lib/workflows/persistence/utils.ts @@ -1,7 +1,6 @@ import crypto from 'crypto' import { db, - webhook, workflow, workflowBlocks, workflowDeploymentVersion, @@ -22,7 +21,6 @@ import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/w const logger = createLogger('WorkflowDBHelpers') export type WorkflowDeploymentVersion = InferSelectModel -type WebhookRecord = InferSelectModel type SubflowInsert = InferInsertModel export interface WorkflowDeploymentVersionResponse { @@ -337,18 +335,6 @@ export async function saveWorkflowToNormalizedTables( // Start a transaction await db.transaction(async (tx) => { - // Snapshot existing webhooks before deletion to preserve them through the cycle - let existingWebhooks: WebhookRecord[] = [] - try { - existingWebhooks = await tx.select().from(webhook).where(eq(webhook.workflowId, workflowId)) - } catch (webhookError) { - // Webhook table might not be available in test environments - logger.debug('Could not load webhooks before save, skipping preservation', { - error: webhookError instanceof Error ? webhookError.message : String(webhookError), - }) - } - - // Clear existing data for this workflow await Promise.all([ tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId)), tx.delete(workflowEdges).where(eq(workflowEdges.workflowId, workflowId)), @@ -419,42 +405,6 @@ export async function saveWorkflowToNormalizedTables( if (subflowInserts.length > 0) { await tx.insert(workflowSubflows).values(subflowInserts) } - - // Re-insert preserved webhooks if any exist and their blocks still exist - if (existingWebhooks.length > 0) { - try { - const webhookInserts = existingWebhooks - .filter((wh) => !!state.blocks?.[wh.blockId ?? '']) - .map((wh) => ({ - id: wh.id, - workflowId: wh.workflowId, - blockId: wh.blockId, - path: wh.path, - provider: wh.provider, - providerConfig: wh.providerConfig, - credentialSetId: wh.credentialSetId, - isActive: wh.isActive, - createdAt: wh.createdAt, - updatedAt: new Date(), - })) - - if (webhookInserts.length > 0) { - await tx.insert(webhook).values(webhookInserts) - logger.debug(`Preserved ${webhookInserts.length} webhook(s) through workflow save`, { - workflowId, - }) - } - } catch (webhookInsertError) { - // Webhook preservation is optional - don't fail the entire save if it errors - logger.warn('Could not preserve webhooks during save', { - error: - webhookInsertError instanceof Error - ? webhookInsertError.message - : String(webhookInsertError), - workflowId, - }) - } - } }) return { success: true } diff --git a/apps/sim/socket/database/operations.ts b/apps/sim/socket/database/operations.ts index 3f30bea034..5fa69f8d98 100644 --- a/apps/sim/socket/database/operations.ts +++ b/apps/sim/socket/database/operations.ts @@ -1,7 +1,6 @@ import * as schema from '@sim/db' import { webhook, workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db' import { createLogger } from '@sim/logger' -import type { InferSelectModel } from 'drizzle-orm' import { and, eq, inArray, or, sql } from 'drizzle-orm' import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' @@ -1175,14 +1174,6 @@ async function handleWorkflowOperationTx( parallelCount: Object.keys(parallels || {}).length, }) - // Snapshot existing webhooks before deletion to preserve them through the cycle - // (workflowBlocks has CASCADE DELETE to webhook table) - const existingWebhooks = await tx - .select() - .from(webhook) - .where(eq(webhook.workflowId, workflowId)) - - // Delete all existing blocks (this will cascade delete edges and webhooks via ON DELETE CASCADE) await tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId)) // Delete all existing subflows @@ -1248,32 +1239,6 @@ async function handleWorkflowOperationTx( await tx.insert(workflowSubflows).values(parallelValues) } - // Re-insert preserved webhooks if any exist and their blocks still exist - type WebhookRecord = InferSelectModel - if (existingWebhooks.length > 0) { - const webhookInserts = existingWebhooks - .filter((wh: WebhookRecord) => !!blocks?.[wh.blockId ?? '']) - .map((wh: WebhookRecord) => ({ - id: wh.id, - workflowId: wh.workflowId, - blockId: wh.blockId, - path: wh.path, - provider: wh.provider, - providerConfig: wh.providerConfig, - credentialSetId: wh.credentialSetId, - isActive: wh.isActive, - createdAt: wh.createdAt, - updatedAt: new Date(), - })) - - if (webhookInserts.length > 0) { - await tx.insert(webhook).values(webhookInserts) - logger.debug(`Preserved ${webhookInserts.length} webhook(s) through state replacement`, { - workflowId, - }) - } - } - logger.info(`Successfully replaced workflow state for ${workflowId}`) break } From 75898c69edd5b20c082587785d467cee468f488c Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 16 Jan 2026 20:07:20 -0800 Subject: [PATCH 4/5] fix(start): seed initial subblock values on batch add (#2864) --- .../[workspaceId]/w/[workflowId]/workflow.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index ec6274a10e..f2a0db4d7a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -700,7 +700,23 @@ const WorkflowContent = React.memo(() => { triggerMode, }) - collaborativeBatchAddBlocks([block], autoConnectEdge ? [autoConnectEdge] : [], {}, {}, {}) + const subBlockValues: Record> = {} + if (block.subBlocks && Object.keys(block.subBlocks).length > 0) { + subBlockValues[id] = {} + for (const [subBlockId, subBlock] of Object.entries(block.subBlocks)) { + if (subBlock.value !== null && subBlock.value !== undefined) { + subBlockValues[id][subBlockId] = subBlock.value + } + } + } + + collaborativeBatchAddBlocks( + [block], + autoConnectEdge ? [autoConnectEdge] : [], + {}, + {}, + subBlockValues + ) usePanelEditorStore.getState().setCurrentBlockId(id) }, [collaborativeBatchAddBlocks, setSelectedEdges] From 5de7228dd927f4f23f6fb171527aad0a8d10e1fa Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 16 Jan 2026 20:17:07 -0800 Subject: [PATCH 5/5] improvement(avatar): use selection-update as the source of truth for presence, ignore other socket ops (#2866) * improvement(avatar): use selection-update as the source of truth for presence, ignore other socket ops * added logs --- .../workspace/providers/socket-provider.tsx | 42 +++++++------------ 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/apps/sim/app/workspace/providers/socket-provider.tsx b/apps/sim/app/workspace/providers/socket-provider.tsx index f173864273..434af6891b 100644 --- a/apps/sim/app/workspace/providers/socket-provider.tsx +++ b/apps/sim/app/workspace/providers/socket-provider.tsx @@ -406,21 +406,13 @@ export function SocketProvider({ children, user }: SocketProviderProps) { socketInstance.on('cursor-update', (data) => { setPresenceUsers((prev) => { const existingIndex = prev.findIndex((user) => user.socketId === data.socketId) - if (existingIndex !== -1) { - return prev.map((user) => - user.socketId === data.socketId ? { ...user, cursor: data.cursor } : user - ) + if (existingIndex === -1) { + logger.debug('Received cursor-update for unknown user', { socketId: data.socketId }) + return prev } - return [ - ...prev, - { - socketId: data.socketId, - userId: data.userId, - userName: data.userName, - avatarUrl: data.avatarUrl, - cursor: data.cursor, - }, - ] + return prev.map((user) => + user.socketId === data.socketId ? { ...user, cursor: data.cursor } : user + ) }) eventHandlers.current.cursorUpdate?.(data) }) @@ -428,21 +420,15 @@ export function SocketProvider({ children, user }: SocketProviderProps) { socketInstance.on('selection-update', (data) => { setPresenceUsers((prev) => { const existingIndex = prev.findIndex((user) => user.socketId === data.socketId) - if (existingIndex !== -1) { - return prev.map((user) => - user.socketId === data.socketId ? { ...user, selection: data.selection } : user - ) - } - return [ - ...prev, - { + if (existingIndex === -1) { + logger.debug('Received selection-update for unknown user', { socketId: data.socketId, - userId: data.userId, - userName: data.userName, - avatarUrl: data.avatarUrl, - selection: data.selection, - }, - ] + }) + return prev + } + return prev.map((user) => + user.socketId === data.socketId ? { ...user, selection: data.selection } : user + ) }) eventHandlers.current.selectionUpdate?.(data) })