Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
<p align="center">
<a href="https://sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/sim.ai-6F3DFA" alt="Sim.ai"></a>
<a href="https://discord.gg/Hr4UWYEcTT" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Discord-Join%20Server-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
<a href="https://x.com/simdotai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/twitter/follow/simstudioai?style=social" alt="Twitter"></a>
<a href="https://docs.sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Docs-6F3DFA.svg" alt="Documentation"></a> <a href="https://deepwiki.com/simstudioai/sim" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/DeepWiki-1E90FF.svg" alt="DeepWiki"></a>
<a href="https://x.com/simdotai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/twitter/follow/simdotai?style=social" alt="Twitter"></a>
<a href="https://docs.sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Docs-6F3DFA.svg" alt="Documentation"></a>
</p>

<p align="center">
<a href="https://cursor.com/link/prompt?text=Help%20me%20set%20up%20Sim%20Studio%20locally.%20Follow%20these%20steps%3A%0A%0A1.%20First%2C%20verify%20Docker%20is%20installed%20and%20running%3A%0A%20%20%20docker%20--version%0A%20%20%20docker%20info%0A%0A2.%20Clone%20the%20repository%3A%0A%20%20%20git%20clone%20https%3A%2F%2Fgithub.com%2Fsimstudioai%2Fsim.git%0A%20%20%20cd%20sim%0A%0A3.%20Start%20the%20services%20with%20Docker%20Compose%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20up%20-d%0A%0A4.%20Wait%20for%20all%20containers%20to%20be%20healthy%20(this%20may%20take%201-2%20minutes)%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20ps%0A%0A5.%20Verify%20the%20app%20is%20accessible%20at%20http%3A%2F%2Flocalhost%3A3000%0A%0AIf%20there%20are%20any%20errors%2C%20help%20me%20troubleshoot%20them.%20Common%20issues%3A%0A-%20Port%203000%2C%203002%2C%20or%205432%20already%20in%20use%0A-%20Docker%20not%20running%0A-%20Insufficient%20memory%20(needs%2012GB%2B%20RAM)%0A%0AFor%20local%20AI%20models%20with%20Ollama%2C%20use%20this%20instead%20of%20step%203%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.ollama.yml%20--profile%20setup%20up%20-d"><img src="https://img.shields.io/badge/Set%20Up%20with-Cursor-000000?logo=cursor&logoColor=white" alt="Set Up with Cursor"></a>
<a href="https://deepwiki.com/simstudioai/sim" target="_blank" rel="noopener noreferrer"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a> <a href="https://cursor.com/link/prompt?text=Help%20me%20set%20up%20Sim%20Studio%20locally.%20Follow%20these%20steps%3A%0A%0A1.%20First%2C%20verify%20Docker%20is%20installed%20and%20running%3A%0A%20%20%20docker%20--version%0A%20%20%20docker%20info%0A%0A2.%20Clone%20the%20repository%3A%0A%20%20%20git%20clone%20https%3A%2F%2Fgithub.com%2Fsimstudioai%2Fsim.git%0A%20%20%20cd%20sim%0A%0A3.%20Start%20the%20services%20with%20Docker%20Compose%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20up%20-d%0A%0A4.%20Wait%20for%20all%20containers%20to%20be%20healthy%20(this%20may%20take%201-2%20minutes)%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20ps%0A%0A5.%20Verify%20the%20app%20is%20accessible%20at%20http%3A%2F%2Flocalhost%3A3000%0A%0AIf%20there%20are%20any%20errors%2C%20help%20me%20troubleshoot%20them.%20Common%20issues%3A%0A-%20Port%203000%2C%203002%2C%20or%205432%20already%20in%20use%0A-%20Docker%20not%20running%0A-%20Insufficient%20memory%20(needs%2012GB%2B%20RAM)%0A%0AFor%20local%20AI%20models%20with%20Ollama%2C%20use%20this%20instead%20of%20step%203%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.ollama.yml%20--profile%20setup%20up%20-d"><img src="https://img.shields.io/badge/Set%20Up%20with-Cursor-000000?logo=cursor&logoColor=white" alt="Set Up with Cursor"></a>
</p>

### Build Workflows with Ease
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,17 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }
)
})

export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps<NoteBlockNodeData>) {
export const NoteBlock = memo(function NoteBlock({
id,
data,
selected,
}: NodeProps<NoteBlockNodeData>) {
const { type, config, name } = data

const { activeWorkflowId, isEnabled, handleClick, hasRing, ringStyles } = useBlockVisual({
blockId: id,
data,
isSelected: selected,
})
const storedValues = useSubBlockStore(
useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SubflowNodeData>) => {
export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<SubflowNodeData>) => {
const { getNodes } = useReactFlow()
const blockRef = useRef<HTMLDivElement>(null)
const userPermissions = useUserPermissionsContext()
Expand Down Expand Up @@ -134,13 +134,15 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps<SubflowNodeDat

/**
* Determine the ring styling based on subflow state priority:
* 1. Focused (selected in editor) or preview selected - blue ring
* 1. Focused (selected in editor), selected (shift-click/box), or preview selected - blue ring
* 2. Diff status (version comparison) - green/orange ring
*/
const hasRing = isFocused || isPreviewSelected || diffStatus === 'new' || diffStatus === 'edited'
const isSelected = !isPreview && selected
const hasRing =
isFocused || isSelected || isPreviewSelected || diffStatus === 'new' || diffStatus === 'edited'
const ringStyles = cn(
hasRing && 'ring-[1.75px]',
(isFocused || isPreviewSelected) && 'ring-[var(--brand-secondary)]',
(isFocused || isSelected || isPreviewSelected) && 'ring-[var(--brand-secondary)]',
diffStatus === 'new' && 'ring-[var(--brand-tertiary-2)]',
diffStatus === 'edited' && 'ring-[var(--warning)]'
)
Expand All @@ -167,7 +169,7 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps<SubflowNodeDat
data-node-id={id}
data-type='subflowNode'
data-nesting-level={nestingLevel}
data-subflow-selected={isFocused || isPreviewSelected}
data-subflow-selected={isFocused || isSelected || isPreviewSelected}
>
{!isPreview && (
<ActionBar blockId={id} blockType={data.kind} disabled={!userPermissions.canEdit} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -557,6 +556,7 @@ const SubBlockRow = ({
export const WorkflowBlock = memo(function WorkflowBlock({
id,
data,
selected,
}: NodeProps<WorkflowBlockProps>) {
const { type, config, name, isPending } = data

Expand All @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand All @@ -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

Expand All @@ -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'
Expand All @@ -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,
Expand All @@ -78,6 +85,7 @@ export function useBlockVisual({ blockId, data, isPending = false }: UseBlockVis
runPathStatus,
isPreview,
isPreviewSelected,
isSelected,
]
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand All @@ -32,11 +34,13 @@ export function getBlockRingStyles(options: BlockRingOptions): {
diffStatus,
runPathStatus,
isPreviewSelection,
isSelected,
} = options

const hasRing =
isExecuting ||
isEditorOpen ||
isSelected ||
isPending ||
diffStatus === 'new' ||
diffStatus === 'edited' ||
Expand All @@ -46,39 +50,53 @@ 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' &&
'ring-[var(--warning)]',
// Run path states (lowest priority - only show if no other states active)
!isExecuting &&
!isEditorOpen &&
!isSelected &&
!isPending &&
!isDeletedBlock &&
!diffStatus &&
runPathStatus === 'success' &&
'ring-[var(--border-success)]',
!isExecuting &&
!isEditorOpen &&
!isSelected &&
!isPending &&
!isDeletedBlock &&
!diffStatus &&
Expand Down
18 changes: 17 additions & 1 deletion apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,23 @@ const WorkflowContent = React.memo(() => {
triggerMode,
})

collaborativeBatchAddBlocks([block], autoConnectEdge ? [autoConnectEdge] : [], {}, {}, {})
const subBlockValues: Record<string, Record<string, unknown>> = {}
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]
Expand Down
42 changes: 14 additions & 28 deletions apps/sim/app/workspace/providers/socket-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -406,43 +406,29 @@ 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)
})

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)
})
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2499,7 +2499,9 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, any> = {
async execute(params: EditWorkflowParams, context?: { userId: string }): Promise<any> {
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', {
Expand Down
50 changes: 0 additions & 50 deletions apps/sim/lib/workflows/persistence/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import crypto from 'crypto'
import {
db,
webhook,
workflow,
workflowBlocks,
workflowDeploymentVersion,
Expand All @@ -22,7 +21,6 @@ import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/w
const logger = createLogger('WorkflowDBHelpers')

export type WorkflowDeploymentVersion = InferSelectModel<typeof workflowDeploymentVersion>
type WebhookRecord = InferSelectModel<typeof webhook>
type SubflowInsert = InferInsertModel<typeof workflowSubflows>

export interface WorkflowDeploymentVersionResponse {
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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 }
Expand Down
Loading
Loading