Generate
@@ -1244,6 +1245,7 @@ function SubBlockComponent({
onMouseDown={handleMouseDown}
data-workflow-search-subblock-id={config.id}
data-workflow-search-canonical-id={config.canonicalParamId ?? config.id}
+ data-preview-readonly={(isPreview && !disabled) || undefined}
className='subblock-content flex flex-col gap-2.5'
>
{renderLabel(
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx
index 61bd02da17b..9222584ba9e 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx
@@ -24,6 +24,7 @@ import { useParams } from 'next/navigation'
import { usePostHog } from 'posthog-js/react'
import { useShallow } from 'zustand/react/shallow'
import { useStoreWithEqualityFn } from 'zustand/traditional'
+import { WorkflowPreviewAction } from '@/components/workflow/workflow-preview-action'
import { isMcpRuntimeReference } from '@/lib/mcp/operation-policy'
import { resolveMcpBlockConfig } from '@/lib/mcp/workflow-config'
import { captureEvent } from '@/lib/posthog/client'
@@ -610,21 +611,12 @@ export function Editor() {
lightweight
/>
-
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx
index 7eb787be542..daa24590d8e 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx
@@ -66,7 +66,8 @@ const OutputCodeContent = React.memo(function OutputCodeContent({
code={code}
showGutter
language={language}
- className='m-0 min-h-full rounded-none border-0 bg-[var(--bg)] dark:bg-[var(--bg)]'
+ appearance='flat'
+ className='m-0 min-h-full'
paddingLeft={8}
gutterStyle={{ backgroundColor: 'transparent' }}
wrapText={wrapText}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx
index b51a54f5277..bba86b895e8 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx
@@ -30,6 +30,7 @@ import {
import { formatDuration } from '@sim/utils/formatting'
import { ReactFlowProvider } from '@xyflow/react'
import { useParams } from 'next/navigation'
+import { WorkflowPreviewAction } from '@/components/workflow/workflow-preview-action'
import { extractReferencePrefixes } from '@/lib/workflows/sanitization/references'
import {
buildCanonicalIndexForSurface,
@@ -42,6 +43,7 @@ import {
import { DELETED_WORKFLOW_LABEL } from '@/lib/workflows/workflow-labels'
import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components'
import { PreviewContextMenu } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-context-menu'
+import { READONLY_PREVIEW_STYLES } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-readonly-styles'
import { PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow'
import { getBlock } from '@/blocks'
import { BlockTile } from '@/blocks/block-tile'
@@ -53,31 +55,6 @@ import { useCodeViewerFeatures } from '@/hooks/use-code-viewer'
import { useContextMenu } from '@/hooks/use-context-menu'
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
-/**
- * CSS override to show full opacity and prevent interaction in readonly preview mode.
- * Extracted to avoid duplicating the style block in multiple places.
- */
-const READONLY_PREVIEW_STYLES = `
- .readonly-preview,
- .readonly-preview * {
- cursor: default !important;
- }
- .readonly-preview [disabled],
- .readonly-preview [data-disabled],
- .readonly-preview input,
- .readonly-preview textarea,
- .readonly-preview [role="combobox"],
- .readonly-preview [role="slider"],
- .readonly-preview [role="switch"],
- .readonly-preview [role="checkbox"] {
- opacity: 1 !important;
- pointer-events: none;
- }
- .readonly-preview .opacity-50 {
- opacity: 1 !important;
- }
-`
-
/**
* Format a value for display as JSON string
*/
@@ -605,7 +582,7 @@ function SubflowConfigDisplay({ block, loop, parallel }: SubflowConfigDisplayPro
return (
{/* Type Selection - matches SubflowEditor */}
-
+
@@ -628,7 +605,7 @@ function SubflowConfigDisplay({ block, loop, parallel }: SubflowConfigDisplayPro
{isCountMode ? (
-
+
-
-
-
-
-
- {isExecutionMode && onDrillDown ? 'Expand workflow' : 'Open in new tab'}
-
-
+
+ {isExecutionMode && onDrillDown ? (
+
+ ) : (
+
+ )}
+
>
) : (
@@ -1443,7 +1409,6 @@ function PreviewEditorContent({
...subBlockValues,
__canonicalModes: canonicalModeOverrides,
}}
- disabled={true}
/>
{index < visibleSubBlocks.length - 1 && (
({ useParams: () => ({ workspaceId: 'workspace-1' }) }))
+vi.mock('@/hooks/use-webhook-management', () => ({
+ useWebhookManagement: () => ({ webhookUrl: null }),
+}))
+vi.mock(
+ '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value',
+ () => ({ useSubBlockValue: () => [undefined, vi.fn()] })
+)
+
+import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
+import { READONLY_PREVIEW_STYLES } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-readonly-styles'
+import type { SubBlockConfig } from '@/blocks/types'
+
+const config: SubBlockConfig = { id: 'enabled', type: 'switch', title: 'Enabled' }
+
+let container: HTMLDivElement
+let root: Root
+
+beforeEach(() => {
+ ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
+ container = document.createElement('div')
+ document.body.appendChild(container)
+ root = createRoot(container)
+})
+
+afterEach(() => {
+ act(() => root.unmount())
+ container.remove()
+})
+
+describe('workflow preview read-only appearance', () => {
+ it('shows a preview value at full opacity while retaining the disabled interaction state', () => {
+ act(() =>
+ root.render(
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+ )
+
+ const preview = container.querySelector('[data-testid="preview"]')!
+ const previewSwitch = preview.querySelector('[role="switch"]') as HTMLButtonElement
+ const disabled = container.querySelector('[data-testid="disabled"]')!
+ const disabledSwitch = disabled.querySelector('[role="switch"]') as HTMLButtonElement
+
+ expect(preview.querySelector('[data-preview-readonly]')).not.toBeNull()
+ expect(previewSwitch.hasAttribute('disabled')).toBe(true)
+ expect(previewSwitch.getAttribute('aria-checked')).toBe('true')
+ expect(getComputedStyle(previewSwitch).opacity).toBe('1')
+ expect(getComputedStyle(previewSwitch).pointerEvents).toBe('none')
+ const removeButton = preview.querySelector('button:not([role="switch"])') as HTMLButtonElement
+ expect(getComputedStyle(removeButton).pointerEvents).toBe('none')
+ expect(getComputedStyle(removeButton).opacity).toBe('0.5')
+
+ act(() => {
+ previewSwitch.click()
+ previewSwitch.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }))
+ })
+ expect(previewSwitch.getAttribute('aria-checked')).toBe('true')
+
+ expect(disabled.querySelector('[data-preview-readonly]')).toBeNull()
+ expect(disabledSwitch.hasAttribute('disabled')).toBe(true)
+ expect(getComputedStyle(disabledSwitch).opacity).toBe('0.5')
+ })
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-readonly-styles.ts b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-readonly-styles.ts
new file mode 100644
index 00000000000..3fe8107450f
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-readonly-styles.ts
@@ -0,0 +1,27 @@
+/**
+ * Keep preview fields noninteractive while showing intentionally read-only controls
+ * and their marked value overlays at full opacity in both preview sections.
+ */
+export const READONLY_PREVIEW_STYLES = `
+ .readonly-preview,
+ .readonly-preview * {
+ cursor: default !important;
+ }
+ .readonly-preview [data-preview-readonly] :is(
+ input,
+ textarea,
+ [role="combobox"],
+ [role="slider"],
+ [role="switch"],
+ [role="checkbox"]
+ ) {
+ opacity: 1 !important;
+ pointer-events: none;
+ }
+ .readonly-preview [data-preview-readonly] :is(button, [role="button"]) {
+ pointer-events: none;
+ }
+ .readonly-preview [data-preview-readonly] [data-preview-full-opacity] {
+ opacity: 1 !important;
+ }
+`
diff --git a/apps/sim/components/workflow/workflow-preview-action.tsx b/apps/sim/components/workflow/workflow-preview-action.tsx
new file mode 100644
index 00000000000..96ee8275afd
--- /dev/null
+++ b/apps/sim/components/workflow/workflow-preview-action.tsx
@@ -0,0 +1,33 @@
+'use client'
+
+import { type ComponentProps, forwardRef } from 'react'
+import { OverlayActionButton, Tooltip } from '@sim/emcn'
+
+interface WorkflowPreviewActionProps
+ extends Omit<
+ ComponentProps,
+ 'size' | 'type' | 'className' | 'shape'
+ > {
+ 'aria-label': string
+}
+
+/** Overlay corner action shared by embedded workflow previews. */
+export const WorkflowPreviewAction = forwardRef(
+ ({ 'aria-label': label, ...props }, ref) => (
+
+
+
+
+ {label}
+
+ )
+)
+
+WorkflowPreviewAction.displayName = 'WorkflowPreviewAction'
diff --git a/packages/emcn/src/components/code/code.test.tsx b/packages/emcn/src/components/code/code.test.tsx
index ebe7d5b1ea6..f9b249ab21c 100644
--- a/packages/emcn/src/components/code/code.test.tsx
+++ b/packages/emcn/src/components/code/code.test.tsx
@@ -1,10 +1,10 @@
/**
* @vitest-environment jsdom
*/
-import { act } from 'react'
+import { act, createRef } from 'react'
import { sleep } from '@sim/utils/helpers'
import { createRoot, type Root } from 'react-dom/client'
-import { afterEach, beforeEach, describe, expect, it } from 'vitest'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Code } from './code'
let root: Root | null = null
@@ -12,6 +12,14 @@ let host: HTMLDivElement | null = null
beforeEach(() => {
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
+ vi.stubGlobal(
+ 'ResizeObserver',
+ class {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ }
+ )
host = document.createElement('div')
document.body.appendChild(host)
root = createRoot(host)
@@ -20,6 +28,7 @@ beforeEach(() => {
afterEach(() => {
if (root) act(() => root?.unmount())
host?.remove()
+ vi.unstubAllGlobals()
root = null
host = null
})
@@ -60,3 +69,79 @@ describe('Code.Viewer workflow references', () => {
expect(host?.querySelector('[data-search-match]')?.textContent).toBe('result')
})
})
+
+describe('Code.Viewer appearances', () => {
+ for (const virtualized of [false, true]) {
+ it(`applies the inspection surface to ${virtualized ? 'virtualized' : 'standard'} output`, async () => {
+ await act(async () => {
+ root?.render(
+
+ )
+ await sleep(1)
+ })
+
+ const viewer = host?.firstElementChild
+ expect(viewer?.classList.contains('rounded-md')).toBe(true)
+ expect(viewer?.classList.contains('border-0')).toBe(true)
+ expect(viewer?.classList.contains('bg-[var(--surface-4)]!')).toBe(true)
+ expect(viewer?.classList.contains('dark:bg-[var(--surface-3)]!')).toBe(true)
+ expect(viewer?.classList.contains('max-h-[300px]')).toBe(true)
+ })
+ }
+
+ it('keeps the flat viewer separate from the default code container', async () => {
+ await act(async () => {
+ root?.render(
+ <>
+
+
+ >
+ )
+ await sleep(1)
+ })
+
+ const [defaultViewer, flatViewer] = Array.from(host?.children ?? [])
+ expect(defaultViewer.classList.contains('rounded-sm')).toBe(true)
+ expect(flatViewer.classList.contains('rounded-none')).toBe(true)
+ expect(flatViewer.classList.contains('bg-[var(--bg)]')).toBe(true)
+ expect(flatViewer.classList.contains('dark:bg-[var(--bg)]')).toBe(true)
+ expect(flatViewer.textContent).toContain('flat')
+ })
+
+ it('applies flat chrome on the virtualized gutter path used by the terminal', async () => {
+ const contentRef = createRef()
+ await act(async () => {
+ root?.render(
+
+ )
+ await sleep(1)
+ })
+
+ const viewer = host?.firstElementChild
+ expect(contentRef.current).toBe(viewer)
+ expect(viewer?.classList.contains('rounded-none')).toBe(true)
+ expect(viewer?.classList.contains('border-0')).toBe(true)
+ expect(viewer?.classList.contains('bg-[var(--bg)]')).toBe(true)
+ expect(viewer?.classList.contains('dark:bg-[var(--bg)]')).toBe(true)
+ expect(viewer?.classList.contains('overflow-x-hidden')).toBe(true)
+ expect(viewer?.classList.contains('min-h-full')).toBe(true)
+ expect(viewer?.classList.contains('rounded-sm')).toBe(false)
+ })
+})
diff --git a/packages/emcn/src/components/code/code.tsx b/packages/emcn/src/components/code/code.tsx
index 2e540301caa..2f0a42a5781 100644
--- a/packages/emcn/src/components/code/code.tsx
+++ b/packages/emcn/src/components/code/code.tsx
@@ -13,6 +13,7 @@ import {
import { escapeRegExp } from '@sim/utils/string'
import { findWorkflowReferenceTokens } from '@sim/utils/workflow-references'
import { useVirtualizer } from '@tanstack/react-virtual'
+import { cva, type VariantProps } from 'class-variance-authority'
import { ChevronRight } from '../../icons'
import { cn } from '../../lib/cn'
import './code.css'
@@ -856,6 +857,18 @@ function applySearchHighlightingToLine(
*/
type CodeViewerDensity = 'default' | 'compact'
+/** Container appearances shared by the standard and virtualized viewers. */
+export const codeViewerAppearanceVariants = cva('', {
+ variants: {
+ appearance: {
+ default: '',
+ inspection: 'rounded-md border-0 bg-[var(--surface-4)]! dark:bg-[var(--surface-3)]!',
+ flat: 'rounded-none border-0 bg-[var(--bg)] dark:bg-[var(--bg)]',
+ },
+ },
+ defaultVariants: { appearance: 'default' },
+})
+
interface CodeViewerProps {
/** Code content to display */
code: string
@@ -865,6 +878,8 @@ interface CodeViewerProps {
language?: 'javascript' | 'json' | 'python' | 'bash' | 'toml'
/** Additional CSS classes for the container */
className?: string
+ /** Container appearance for code inspected in logs/previews or on flat surfaces. */
+ appearance?: NonNullable['appearance']>
/** Visual density for read-only code. */
density?: CodeViewerDensity
/** Highlight Sim `{{ENV}}` and `` references with the platform accent. */
@@ -948,6 +963,7 @@ type ViewerInnerProps = {
language: 'javascript' | 'json' | 'python' | 'bash' | 'toml'
/** Additional CSS classes for the container */
className?: string
+ appearance: NonNullable
/** Visual density for read-only code. */
density: CodeViewerDensity
highlightWorkflowReferences: boolean
@@ -978,6 +994,7 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({
showGutter,
language,
className,
+ appearance,
density,
highlightWorkflowReferences,
paddingLeft,
@@ -1147,6 +1164,7 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({
wrapText ? 'overflow-x-hidden' : 'overflow-x-auto',
'overflow-y-auto',
'dark:bg-[var(--code-bg)]',
+ codeViewerAppearanceVariants({ appearance }),
className
)}
style={{ height: containerHeight }}
@@ -1196,6 +1214,7 @@ const ViewerInner = memo(function ViewerInner({
showGutter,
language,
className,
+ appearance,
density,
highlightWorkflowReferences,
paddingLeft,
@@ -1309,7 +1328,7 @@ const ViewerInner = memo(function ViewerInner({
// Grid-based rendering for gutter alignment (works with wrap)
if (showGutter) {
return (
-
+