Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d2a1cc4
improvement(workflow): refine canvas interactions and rendering
andresdjasso Jul 24, 2026
451be0c
fix(workflow): keep outputs on the right, focus newly created blocks
andresdjasso Jul 27, 2026
8c9122e
fix(workflow): floor header-only card height, adopt brand tag palette
andresdjasso Jul 28, 2026
418aab0
improvement(workflow): polish workflow canvas interactions
andresdjasso Aug 1, 2026
7ca132d
fix(workflow): restyle loop drop target outline
andresdjasso Aug 1, 2026
898063b
fix(workflow): shorten human block catalog label
andresdjasso Aug 1, 2026
6bbb2eb
fix(workflow): canonicalize realtime edge handles
andresdjasso Aug 1, 2026
ec833c1
improvement(notes): add focused canvas editing
andresdjasso Aug 3, 2026
643c318
improvement(workflow): refine live execution feedback
andresdjasso Aug 4, 2026
d2dbf89
merge staging into workflow-updates
andresdjasso Aug 4, 2026
0476a6d
fix(workflow): align running action loader
andresdjasso Aug 4, 2026
4c6bfb9
fix(workflow): preserve running control artwork
andresdjasso Aug 4, 2026
d4de73b
feat(workflow): unify core block colors
andresdjasso Aug 5, 2026
44adfaf
fix(workflow): neutralize content block color
andresdjasso Aug 5, 2026
a49c532
fix(workflow): lighten content block tone
andresdjasso Aug 5, 2026
66bfb91
fix(workflow): align content block ink
andresdjasso Aug 5, 2026
9cd9fe6
fix(workflow): update content block teal
andresdjasso Aug 5, 2026
dfbb623
fix(workflow): brighten content block teal
andresdjasso Aug 5, 2026
234728c
fix(workflow): replace legacy deployments icon
andresdjasso Aug 5, 2026
cce22de
fix(workflows): apply semantic colors to native triggers
andresdjasso Aug 5, 2026
1e80dd9
fix(workflows): theme running stop hover in dark mode
andresdjasso Aug 5, 2026
6b1d733
fix(workflows): suppress hidden action tooltips while running
andresdjasso Aug 5, 2026
ccf1ce7
fix(workflows): blend running loader into execution swell
andresdjasso Aug 5, 2026
a257119
fix(sidebar): show route workspace identity fallback
andresdjasso Aug 5, 2026
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
125 changes: 91 additions & 34 deletions apps/realtime/src/database/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import {
isKnownWorkflowTriggerBlock,
isWorkflowAnnotationOnlyBlockType,
isWorkflowBlockProtected,
normalizeWorkflowEdgeSourceHandle,
normalizeWorkflowEdgeTargetHandle,
} from '@sim/workflow-types/workflow'
import { and, eq, inArray, isNull, or, sql } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/postgres-js'
Expand All @@ -57,13 +59,21 @@ function toEdgeHandles(edge: PersistedEdgeRecord) {
}

interface EdgeAddCandidate {
id?: string
id: string
source: string
target: string
sourceHandle?: string | null
targetHandle?: string | null
}

function canonicalizeEdgeAddCandidate(edge: EdgeAddCandidate): EdgeAddCandidate {
return {
...edge,
sourceHandle: normalizeWorkflowEdgeSourceHandle(edge.sourceHandle),
targetHandle: normalizeWorkflowEdgeTargetHandle(edge.targetHandle),
}
}
Comment thread
cursor[bot] marked this conversation as resolved.

interface FilterEdgesForPersistResult<T> {
safeEdges: T[]
droppedCounts: Record<string, number>
Expand Down Expand Up @@ -283,8 +293,8 @@ async function insertAutoConnectEdge(
workflowId,
sourceBlockId: autoConnectEdge.source,
targetBlockId: autoConnectEdge.target,
sourceHandle: autoConnectEdge.sourceHandle || null,
targetHandle: autoConnectEdge.targetHandle || null,
sourceHandle: normalizeWorkflowEdgeSourceHandle(autoConnectEdge.sourceHandle),
targetHandle: normalizeWorkflowEdgeTargetHandle(autoConnectEdge.targetHandle),
})
logger.debug(
`Added auto-connect edge ${autoConnectEdge.id}: ${autoConnectEdge.source} -> ${autoConnectEdge.target}`
Expand Down Expand Up @@ -736,6 +746,33 @@ async function handleBlockOperationTx(
break
}

case BLOCK_OPERATIONS.UPDATE_ERROR_ENABLED: {
if (!payload.id || payload.errorEnabled === undefined) {
throw new Error('Missing required fields for update error enabled operation')
}

const updateResult = await tx
.update(workflowBlocks)
.set({
data: sql`jsonb_set(
coalesce(${workflowBlocks.data}, '{}'::jsonb),
'{errorEnabled}',
${JSON.stringify(payload.errorEnabled)}::jsonb,
true
)`,
updatedAt: new Date(),
})
.where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
.returning({ id: workflowBlocks.id })

if (updateResult.length === 0) {
throw new Error(`Block ${payload.id} not found in workflow ${workflowId}`)
}

logger.debug(`Updated block error output: ${payload.id} -> ${payload.errorEnabled}`)
break
}
Comment thread
cursor[bot] marked this conversation as resolved.

case BLOCK_OPERATIONS.UPDATE_CANONICAL_MODE: {
if (!payload.id || !payload.canonicalId || !payload.canonicalMode) {
throw new Error('Missing required fields for update canonical mode operation')
Expand Down Expand Up @@ -1024,8 +1061,8 @@ async function handleBlocksOperationTx(
// blocksById lookup (a plain `tx.select` from `workflowBlocks`) also
// sees the blocks this same batch just inserted — reads observe a
// transaction's own prior writes.
const candidates: EdgeAddCandidate[] = (edges as Array<Record<string, unknown>>).map(
(e) => ({
const candidates: EdgeAddCandidate[] = (edges as Array<Record<string, unknown>>).map((e) =>
canonicalizeEdgeAddCandidate({
id: e.id as string,
source: e.source as string,
target: e.target as string,
Expand All @@ -1051,8 +1088,8 @@ async function handleBlocksOperationTx(
workflowId,
sourceBlockId: edge.source,
targetBlockId: edge.target,
sourceHandle: edge.sourceHandle || null,
targetHandle: edge.targetHandle || null,
sourceHandle: normalizeWorkflowEdgeSourceHandle(edge.sourceHandle),
targetHandle: normalizeWorkflowEdgeTargetHandle(edge.targetHandle),
}))

await tx
Expand Down Expand Up @@ -1509,18 +1546,17 @@ async function handleEdgeOperationTx(tx: any, workflowId: string, operation: str
throw new Error('Missing required fields for add edge operation')
}

const candidate = canonicalizeEdgeAddCandidate({
id: payload.id,
source: payload.source,
target: payload.target,
sourceHandle: payload.sourceHandle ?? null,
targetHandle: payload.targetHandle ?? null,
})
const { safeEdges, droppedCounts, droppedDuplicates } = await filterEdgesForPersist(
tx,
workflowId,
[
{
id: payload.id,
source: payload.source,
target: payload.target,
sourceHandle: payload.sourceHandle ?? null,
targetHandle: payload.targetHandle ?? null,
},
]
[candidate]
)

if (safeEdges.length === 0) {
Expand All @@ -1534,13 +1570,14 @@ async function handleEdgeOperationTx(tx: any, workflowId: string, operation: str
break
}

const [safeEdge] = safeEdges
await tx.insert(workflowEdges).values({
id: payload.id,
id: safeEdge.id,
workflowId,
sourceBlockId: payload.source,
targetBlockId: payload.target,
sourceHandle: payload.sourceHandle || null,
targetHandle: payload.targetHandle || null,
sourceBlockId: safeEdge.source,
targetBlockId: safeEdge.target,
sourceHandle: normalizeWorkflowEdgeSourceHandle(safeEdge.sourceHandle),
targetHandle: normalizeWorkflowEdgeTargetHandle(safeEdge.targetHandle),
})

logger.debug(`Added edge ${payload.id}: ${payload.source} -> ${payload.target}`)
Expand Down Expand Up @@ -1755,13 +1792,15 @@ async function handleEdgesOperationTx(

logger.info(`Batch adding ${edges.length} edges to workflow ${workflowId}`)

const candidates: EdgeAddCandidate[] = (edges as Array<Record<string, unknown>>).map((e) => ({
id: e.id as string,
source: e.source as string,
target: e.target as string,
sourceHandle: (e.sourceHandle as string | null) ?? null,
targetHandle: (e.targetHandle as string | null) ?? null,
}))
const candidates: EdgeAddCandidate[] = (edges as Array<Record<string, unknown>>).map((e) =>
canonicalizeEdgeAddCandidate({
id: e.id as string,
source: e.source as string,
target: e.target as string,
sourceHandle: (e.sourceHandle as string | null) ?? null,
targetHandle: (e.targetHandle as string | null) ?? null,
})
)

const { safeEdges, droppedCounts, droppedDuplicates, droppedCyclic } =
await filterEdgesForPersist(tx, workflowId, candidates)
Expand All @@ -1784,8 +1823,8 @@ async function handleEdgesOperationTx(
workflowId,
sourceBlockId: edge.source,
targetBlockId: edge.target,
sourceHandle: edge.sourceHandle || null,
targetHandle: edge.targetHandle || null,
sourceHandle: normalizeWorkflowEdgeSourceHandle(edge.sourceHandle),
targetHandle: normalizeWorkflowEdgeTargetHandle(edge.targetHandle),
}))

await tx
Expand Down Expand Up @@ -2152,16 +2191,34 @@ async function handleWorkflowOperationTx(

// Insert all edges from the new state
if (edges && edges.length > 0) {
const edgeValues = edges.map((edge: any) => ({
const canonicalEdges = (edges as Array<Record<string, unknown>>).map((edge) =>
canonicalizeEdgeAddCandidate({
id: edge.id as string,
source: edge.source as string,
target: edge.target as string,
sourceHandle: (edge.sourceHandle as string | null) ?? null,
targetHandle: (edge.targetHandle as string | null) ?? null,
})
)
const uniqueEdges = filterUniqueWorkflowEdges(canonicalEdges, [])
const edgeValues = uniqueEdges.map((edge) => ({
id: edge.id,
workflowId,
sourceBlockId: edge.source,
targetBlockId: edge.target,
sourceHandle: edge.sourceHandle || null,
targetHandle: edge.targetHandle || null,
sourceHandle: edge.sourceHandle ?? null,
targetHandle: edge.targetHandle ?? null,
}))

await tx.insert(workflowEdges).values(edgeValues)
if (uniqueEdges.length < edges.length) {
logger.info(`Dropped ${edges.length - uniqueEdges.length} duplicate edge(s)`, {
operation: WORKFLOW_OPERATIONS.REPLACE_STATE,
})
}

if (edgeValues.length > 0) {
await tx.insert(workflowEdges).values(edgeValues)
}
}

// Insert all loops from the new state
Expand Down
1 change: 1 addition & 0 deletions apps/realtime/src/middleware/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const WRITE_OPERATIONS: string[] = [
BLOCK_OPERATIONS.TOGGLE_ENABLED,
BLOCK_OPERATIONS.UPDATE_PARENT,
BLOCK_OPERATIONS.UPDATE_ADVANCED_MODE,
BLOCK_OPERATIONS.UPDATE_ERROR_ENABLED,
BLOCK_OPERATIONS.UPDATE_CANONICAL_MODE,
BLOCK_OPERATIONS.REPLACE_CANONICAL_MODES,
BLOCK_OPERATIONS.TOGGLE_HANDLES,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
import type { ReactNode } from 'react'
import { createContext, use, useEffect, useMemo, useRef, useState } from 'react'
import { cn } from '@sim/emcn'
import { FROSTED_GLASS_SURFACE } from '@/lib/ui/glass-surface'

/**
* Frosted near-white surface for the scrolled bar - `--bg` at 92% + a strong 40px
* blur, edge to edge. Exported as the single source of truth so the mobile
* dropdown sheet ({@link MobileNav}) wears the exact same glass as the bar and the
* two can never drift.
*/
export const NAVBAR_GLASS_SURFACE =
'bg-[color-mix(in_srgb,var(--bg)_92%,transparent)] backdrop-blur-2xl'
export const NAVBAR_GLASS_SURFACE = FROSTED_GLASS_SURFACE

interface NavbarFrostContextValue {
/**
Expand Down
Loading
Loading