Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f8e47e0
improvement(workflow): refine canvas interactions and rendering
andresdjasso Jul 24, 2026
68491eb
fix(workflow): keep outputs on the right, focus newly created blocks
andresdjasso Jul 27, 2026
6f20689
fix(workflow): floor header-only card height, adopt brand tag palette
andresdjasso Jul 28, 2026
f543974
improvement(workflow): polish workflow canvas interactions
andresdjasso Aug 1, 2026
c930c05
fix(workflow): restyle loop drop target outline
andresdjasso Aug 1, 2026
2ab461f
fix(workflow): shorten human block catalog label
andresdjasso Aug 1, 2026
7f50240
fix(workflow): canonicalize realtime edge handles
andresdjasso Aug 1, 2026
256c97b
improvement(notes): add focused canvas editing
andresdjasso Aug 3, 2026
434adba
improvement(workflow): refine live execution feedback
andresdjasso Aug 4, 2026
3783ff1
fix(workflow): align running action loader
andresdjasso Aug 4, 2026
081a359
fix(workflow): preserve running control artwork
andresdjasso Aug 4, 2026
4b1fab6
feat(workflow): unify core block colors
andresdjasso Aug 5, 2026
76fb897
fix(workflow): neutralize content block color
andresdjasso Aug 5, 2026
5f3c385
fix(workflow): lighten content block tone
andresdjasso Aug 5, 2026
7a42b6f
fix(workflow): align content block ink
andresdjasso Aug 5, 2026
78f6fa8
fix(workflow): update content block teal
andresdjasso Aug 5, 2026
33d7609
fix(workflow): brighten content block teal
andresdjasso Aug 5, 2026
034a107
fix(workflow): replace legacy deployments icon
andresdjasso Aug 5, 2026
1916b8e
fix(workflows): apply semantic colors to native triggers
andresdjasso Aug 5, 2026
773daf8
fix(workflows): theme running stop hover in dark mode
andresdjasso Aug 5, 2026
b8bc0c6
fix(workflows): suppress hidden action tooltips while running
andresdjasso Aug 5, 2026
2ddd441
fix(workflows): blend running loader into execution swell
andresdjasso Aug 5, 2026
04f0ffc
fix(sidebar): show route workspace identity fallback
andresdjasso Aug 5, 2026
ceb0595
improvement(workflow): redesign editor and configuration
andresdjasso Aug 10, 2026
445b351
improvement(workflow): move canvas controls into header
andresdjasso Aug 11, 2026
2aa64c0
improvement(workflow): refine canvas mode controls
andresdjasso Aug 11, 2026
420739a
fix(workflow): restore compact run button
andresdjasso Aug 11, 2026
2bd2692
improvement(editor): separate block header from settings
andresdjasso Aug 11, 2026
de94429
improvement(emcn): add compact chip switch size
andresdjasso Aug 11, 2026
a018173
improvement(workflow): organize toolbar and editor views
andresdjasso Aug 11, 2026
ac9be72
improvement(workflow): refine editor toolbar and empty states
andresdjasso Aug 11, 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
152 changes: 118 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),
}
}

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 @@ -618,6 +628,33 @@ async function handleBlockOperationTx(
break
}

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

const updateResult = await tx
.update(workflowBlocks)
.set({
data: sql`jsonb_set(
coalesce(${workflowBlocks.data}, '{}'::jsonb),
'{description}',
${JSON.stringify(payload.description)}::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 description: ${payload.id}`)
break
}

case BLOCK_OPERATIONS.TOGGLE_ENABLED: {
if (!payload.id) {
throw new Error('Missing block ID for toggle enabled operation')
Expand Down Expand Up @@ -736,6 +773,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
}

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 +1088,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 +1115,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 +1573,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 +1597,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 +1819,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 +1850,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 +2218,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
6 changes: 6 additions & 0 deletions apps/realtime/src/middleware/permissions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,12 @@ describe('checkRolePermission', () => {
{ operation: 'update', adminAllowed: true, writeAllowed: true, readAllowed: false },
{ operation: 'update-position', adminAllowed: true, writeAllowed: true, readAllowed: false },
{ operation: 'update-name', adminAllowed: true, writeAllowed: true, readAllowed: false },
{
operation: 'update-description',
adminAllowed: true,
writeAllowed: true,
readAllowed: false,
},
{ operation: 'toggle-enabled', adminAllowed: true, writeAllowed: true, readAllowed: false },
{ operation: 'update-parent', adminAllowed: true, writeAllowed: true, readAllowed: false },
{
Expand Down
2 changes: 2 additions & 0 deletions apps/realtime/src/middleware/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ const WRITE_OPERATIONS: string[] = [
// Block operations
BLOCK_OPERATIONS.UPDATE_POSITION,
BLOCK_OPERATIONS.UPDATE_NAME,
BLOCK_OPERATIONS.UPDATE_DESCRIPTION,
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
19 changes: 8 additions & 11 deletions apps/sim/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@ import { BrandedLayout } from '@/components/branded-layout'
import { PostHogProvider } from '@/app/_shell/providers/posthog-provider'
import { generateBrandedMetadata, generateThemeCSS } from '@/ee/whitelabeling'
import '@/app/_styles/globals.css'
import {
isChatEnabled,
isHosted,
isReactGrabEnabled,
isReactScanEnabled,
} from '@/lib/core/config/env-flags'
import { isHosted, isReactGrabEnabled, isReactScanEnabled } from '@/lib/core/config/env-flags'
import { DesktopUpdateGate } from '@/app/_shell/desktop-update-gate'
import { HydrationErrorHandler } from '@/app/_shell/hydration-error-handler'
import { QueryProvider } from '@/app/_shell/providers/query-provider'
Expand Down Expand Up @@ -155,10 +150,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
}

var activeTab = panelState && panelState.activeTab;
// A session that used the Chat tab before it was turned off still
// has 'copilot' persisted; without this the CSS hides every tab
// body and the panel paints empty.
if (activeTab === 'copilot' && !${isChatEnabled}) {
// Chat moved out of the right inspector. Migrate the legacy
// persisted tab before first paint so the inspector opens on Blocks.
if (activeTab === 'copilot') {
activeTab = 'toolbar';
}
if (activeTab) {
Expand Down Expand Up @@ -260,7 +254,10 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=

{isHosted ? <PublicEnvScript /> : <RuntimePublicEnvScript disableNextScript />}
</head>
<body className={`${season.variable} font-season`} suppressHydrationWarning>
<body
className={`${season.variable} font-season [--scrollbar-size:4px]`}
suppressHydrationWarning
>
{/* Google Tag Manager (noscript) — hosted only */}
{isHosted && (
<noscript>
Expand Down
Loading