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
2 changes: 1 addition & 1 deletion apps/sim/app/(auth)/signup/signup-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ function SignupFormContent({
disabled={isLoading}
>
<span className='flex items-center gap-1'>
{isLoading ? 'Creating account...' : 'Create account'}
{isLoading ? 'Creating account' : 'Create account'}
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isButtonHovered ? (
<ArrowRight className='h-4 w-4' aria-hidden='true' />
Expand Down
24 changes: 24 additions & 0 deletions apps/sim/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,30 @@
animation: dash-animation 1.5s linear infinite !important;
}

/**
* Active block ring animation - cycles through gray tones using box-shadow
*/
@keyframes ring-pulse-colors {
0%,
100% {
box-shadow: 0 0 0 4px var(--surface-14);
}
33% {
box-shadow: 0 0 0 4px var(--surface-12);
}
66% {
box-shadow: 0 0 0 4px var(--surface-15);
}
}

.dark .animate-ring-pulse {
animation: ring-pulse-colors 2s ease-in-out infinite !important;
}

.light .animate-ring-pulse {
animation: ring-pulse-colors 2s ease-in-out infinite !important;
}
Comment on lines +77 to +99
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: violates custom instruction "avoid editing the globals.css file unless absolutely necessary. move style changes to local component files instead"

Context Used: Context from dashboard - Avoid editing the globals.css file unless absolutely necessary. Move style changes to local componen... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/sim/app/globals.css
Line: 77:99

Comment:
**style:** violates custom instruction "avoid editing the `globals.css` file unless absolutely necessary. move style changes to local component files instead"

**Context Used:** Context from `dashboard` - Avoid editing the globals.css file unless absolutely necessary. Move style changes to local componen... ([source](https://app.greptile.com/review/custom-context?memory=c3b5e4b0-6580-4307-83aa-ba28f105b3c4))

How can I resolve this? If you propose a fix, please make it concise.


/**
* Dark color tokens - single source of truth for all colors (dark-only)
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
'use client'

import { useCallback } from 'react'
import { Layout, LibraryBig, Search } from 'lucide-react'
import Image from 'next/image'
import { useParams, useRouter } from 'next/navigation'
import { Button } from '@/components/emcn'
import { AgentIcon } from '@/components/icons'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { useSearchModalStore } from '@/stores/search-modal/store'

const logger = createLogger('WorkflowCommandList')

/**
* Command item data structure
Expand Down Expand Up @@ -49,13 +55,131 @@ const commands: CommandItem[] = [
* Centered on the screen for empty workflows
*/
export function CommandList() {
const params = useParams()
const router = useRouter()
const { open: openSearchModal } = useSearchModalStore()

const workspaceId = params.workspaceId as string | undefined

/**
* Handle click on a command row.
*
* Mirrors the behavior of the corresponding global keyboard shortcuts:
* - Templates: navigate to workspace templates
* - New Agent: add an agent block to the canvas
* - Logs: navigate to workspace logs
* - Search Blocks: open the universal search modal
*
* @param label - Command label that was clicked.
*/
const handleCommandClick = useCallback(
(label: string) => {
try {
switch (label) {
case 'Templates': {
if (!workspaceId) {
logger.warn('No workspace ID found, cannot navigate to templates from command list')
return
}
router.push(`/workspace/${workspaceId}/templates`)
return
}
case 'New Agent': {
const event = new CustomEvent('add-block-from-toolbar', {
detail: { type: 'agent', enableTriggerMode: false },
})
window.dispatchEvent(event)
return
}
case 'Logs': {
if (!workspaceId) {
logger.warn('No workspace ID found, cannot navigate to logs from command list')
return
}
router.push(`/workspace/${workspaceId}/logs`)
return
}
case 'Search Blocks': {
openSearchModal()
return
}
default:
logger.warn('Unknown command label clicked in command list', { label })
}
} catch (error) {
logger.error('Failed to handle command click in command list', { error, label })
}
},
[router, workspaceId, openSearchModal]
)

/**
* Handle drag-over events from the toolbar.
*
* When a toolbar item is dragged over the command list, mark the drop as valid
* so the browser shows the appropriate drop cursor. Only reacts to toolbar
* drags that carry the expected JSON payload.
*
* @param event - Drag event from the browser.
*/
const handleDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => {
if (!event.dataTransfer?.types.includes('application/json')) {
return
}
event.preventDefault()
event.dataTransfer.dropEffect = 'move'
}, [])

/**
* Handle drops of toolbar items onto the command list.
*
* This forwards the drop information (block type and cursor position)
* to the workflow canvas via a custom event. The workflow component
* then reuses its existing drop logic to place the block precisely
* under the cursor, including container/subflow handling.
*
* @param event - Drop event from the browser.
*/
const handleDrop = useCallback((event: React.DragEvent<HTMLDivElement>) => {
if (!event.dataTransfer?.types.includes('application/json')) {
return
}

event.preventDefault()

try {
const raw = event.dataTransfer.getData('application/json')
if (!raw) return

const data = JSON.parse(raw) as { type?: string; enableTriggerMode?: boolean }
if (!data?.type || data.type === 'connectionBlock') return

const overlayDropEvent = new CustomEvent('toolbar-drop-on-empty-workflow-overlay', {
detail: {
type: data.type,
enableTriggerMode: data.enableTriggerMode ?? false,
clientX: event.clientX,
clientY: event.clientY,
},
})

window.dispatchEvent(overlayDropEvent)
} catch (error) {
logger.error('Failed to handle drop on command list', { error })
}
}, [])

return (
<div
className={cn(
'pointer-events-none absolute inset-0 mb-[50px] flex items-center justify-center'
)}
>
<div className='pointer-events-none flex flex-col gap-[8px]'>
<div
className='pointer-events-auto flex flex-col gap-[8px]'
onDragOver={handleDragOver}
onDrop={handleDrop}
>
{/* Logo */}
<div className='mb-[20px] flex justify-center'>
<Image
Expand All @@ -79,6 +203,7 @@ export function CommandList() {
<div
key={command.label}
className='group flex cursor-pointer items-center justify-between gap-[60px]'
onClick={() => handleCommandClick(command.label)}
>
{/* Left side: Icon and Label */}
<div className='flex items-center gap-[8px]'>
Expand All @@ -91,15 +216,15 @@ export function CommandList() {
{/* Right side: Keyboard Shortcut */}
<div className='flex items-center gap-[4px]'>
<Button
className='group-hover:-translate-y-0.5 w-[26px] py-[3px] text-[12px] hover:translate-y-0 hover:text-[var(--text-tertiary)] hover:shadow-[0_2px_0_0] group-hover:text-[var(--text-primary)] group-hover:shadow-[0_4px_0_0]'
className='group-hover:-translate-y-0.5 w-[26px] py-[3px] text-[12px] hover:translate-y-0 hover:text-[var(--text-tertiary)] hover:shadow-[0_2px_0_0_rgba(48,48,48,1)] group-hover:text-[var(--text-primary)] group-hover:shadow-[0_4px_0_0_rgba(48,48,48,1)]'
variant='3d'
>
<span>⌘</span>
</Button>
{shortcuts.map((key, index) => (
<Button
key={index}
className='group-hover:-translate-y-0.5 w-[26px] py-[3px] text-[12px] hover:translate-y-0 hover:text-[var(--text-tertiary)] hover:shadow-[0_2px_0_0] group-hover:text-[var(--text-primary)] group-hover:shadow-[0_4px_0_0]'
className='group-hover:-translate-y-0.5 w-[26px] py-[3px] text-[12px] hover:translate-y-0 hover:text-[var(--text-tertiary)] hover:shadow-[0_2px_0_0_rgba(48,48,48,1)] group-hover:text-[var(--text-primary)] group-hover:shadow-[0_4px_0_0_rgba(48,48,48,1)]'
variant='3d'
>
{key}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@ interface CursorRenderData {
color: string
}

const POINTER_OFFSET = {
x: 0,
y: 0,
}

const CursorsComponent = () => {
const { presenceUsers } = useSocket()
const viewport = useViewport()
Expand Down Expand Up @@ -60,23 +55,15 @@ const CursorsComponent = () => {
transition: 'transform 0.12s ease-out',
}}
>
<div
className='relative'
style={{ transform: `translate(${-POINTER_OFFSET.x}px, ${-POINTER_OFFSET.y}px)` }}
>
{/* Simple cursor pointer */}
<svg width={16} height={18} viewBox='0 0 16 18' fill='none'>
<path
d='M0.5 0.5L0.5 12L4 9L6.5 15L8.5 14L6 8L12 8L0.5 0.5Z'
fill={color}
stroke='rgba(0,0,0,0.3)'
strokeWidth={1}
/>
<div className='relative flex items-start'>
{/* Filled mouse pointer cursor */}
<svg className='-mt-[18px]' width={24} height={24} viewBox='0 0 24 24' fill={color}>
<path d='M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z' />
</svg>

{/* Name tag underneath and to the right */}
{/* Name tag to the right, background tightly wrapping text */}
<div
className='absolute top-[18px] left-[4px] h-[21px] w-[140px] truncate whitespace-nowrap rounded-[2px] p-[6px] font-medium text-[11px] text-[var(--surface-1)]'
className='ml-[-4px] inline-flex max-w-[160px] truncate whitespace-nowrap rounded-[2px] px-1.5 py-[2px] font-medium text-[11px] text-[var(--surface-1)]'
style={{ backgroundColor: color }}
>
{name}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useMemo } from 'react'
import { cn } from '@/lib/utils'
import { useExecutionStore } from '@/stores/execution/store'
import { usePanelEditorStore } from '@/stores/panel-new/editor/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useBlockState } from '../components/workflow-block/hooks'
Expand Down Expand Up @@ -28,6 +29,10 @@ export function useBlockCore({ blockId, data, isPending = false }: UseBlockCoreO
data
)

// Run path state (from last execution)
const lastRunPath = useExecutionStore((state) => state.lastRunPath)
const runPathStatus = lastRunPath.get(blockId)

// Focus management
const setCurrentBlockId = usePanelEditorStore((state) => state.setCurrentBlockId)
const currentBlockId = usePanelEditorStore((state) => state.currentBlockId)
Expand All @@ -38,27 +43,60 @@ export function useBlockCore({ blockId, data, isPending = false }: UseBlockCoreO
}, [blockId, setCurrentBlockId])

// Ring styling based on all states
// Priority: active (animated) > pending > focused > deleted > diff > run path
const { hasRing, ringStyles } = useMemo(() => {
const hasRing =
isActive ||
isPending ||
isFocused ||
diffStatus === 'new' ||
diffStatus === 'edited' ||
isDeletedBlock
isDeletedBlock ||
!!runPathStatus

const ringStyles = cn(
hasRing && 'ring-[1.75px]',
isActive && 'ring-[#8C10FF] animate-pulse-ring',
isPending && 'ring-[var(--warning)]',
isFocused && 'ring-[var(--brand-secondary)]',
diffStatus === 'new' && 'ring-[#22C55F]',
diffStatus === 'edited' && 'ring-[var(--warning)]',
isDeletedBlock && 'ring-[var(--text-error)]'
// Executing block: animated ring cycling through gray tones (animation handles all styling)
isActive && 'animate-ring-pulse',
// Non-active states use standard ring utilities
!isActive && hasRing && 'ring-[1.75px]',
// Pending state: warning ring
!isActive && isPending && 'ring-[var(--warning)]',
// Focused (selected) state: brand ring
!isActive && !isPending && isFocused && 'ring-[var(--brand-secondary)]',
// Deleted state (highest priority after active/pending/focused)
!isActive && !isPending && !isFocused && isDeletedBlock && 'ring-[var(--text-error)]',
// Diff states
!isActive &&
!isPending &&
!isFocused &&
!isDeletedBlock &&
diffStatus === 'new' &&
'ring-[#22C55E]',
!isActive &&
!isPending &&
!isFocused &&
!isDeletedBlock &&
diffStatus === 'edited' &&
'ring-[var(--warning)]',
// Run path states (lowest priority - only show if no other states active)
!isActive &&
!isPending &&
!isFocused &&
!isDeletedBlock &&
!diffStatus &&
runPathStatus === 'success' &&
'ring-[var(--surface-14)]',
!isActive &&
!isPending &&
!isFocused &&
!isDeletedBlock &&
!diffStatus &&
runPathStatus === 'error' &&
'ring-[var(--text-error)]'
)

return { hasRing, ringStyles }
}, [isActive, isPending, isFocused, diffStatus, isDeletedBlock])
}, [isActive, isPending, isFocused, diffStatus, isDeletedBlock, runPathStatus])

return {
// Workflow context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export function useWorkflowExecution() {
setExecutor,
setDebugContext,
setActiveBlocks,
setBlockRunStatus,
} = useExecutionStore()
const [executionResult, setExecutionResult] = useState<ExecutionResult | null>(null)
const executionStream = useExecutionStream()
Expand Down Expand Up @@ -901,6 +902,9 @@ export function useWorkflowExecution() {
// Create a new Set to trigger React re-render
setActiveBlocks(new Set(activeBlocksSet))

// Track successful block execution in run path
setBlockRunStatus(data.blockId, 'success')

// Add to console
addConsole({
input: data.input || {},
Expand Down Expand Up @@ -933,6 +937,9 @@ export function useWorkflowExecution() {
// Create a new Set to trigger React re-render
setActiveBlocks(new Set(activeBlocksSet))

// Track failed block execution in run path
setBlockRunStatus(data.blockId, 'error')

// Add error to console
addConsole({
input: data.input || {},
Expand Down
Loading