Compare commits

..

4 Commits

Author SHA1 Message Date
Waleed
5de7228dd9 improvement(avatar): use selection-update as the source of truth for presence, ignore other socket ops (#2866)
* improvement(avatar): use selection-update as the source of truth for presence, ignore other socket ops

* added logs
2026-01-16 20:17:07 -08:00
Vikhyath Mondreti
75898c69ed fix(start): seed initial subblock values on batch add (#2864) 2026-01-16 20:07:20 -08:00
Vikhyath Mondreti
b14672887b fix(sockets): webhooks logic removal from copilot ops (#2862)
* fix(sockets): dying on deployed webhooks

* fix edit workflow
2026-01-16 19:53:14 -08:00
Waleed
d024c1e489 fix(shift): fix shift select blue ring fading (#2863) 2026-01-16 19:52:51 -08:00
7 changed files with 79 additions and 44 deletions

View File

@@ -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(

View File

@@ -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()
@@ -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)]'
)
@@ -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} />

View File

@@ -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)) {
@@ -557,6 +556,7 @@ const SubBlockRow = ({
export const WorkflowBlock = memo(function WorkflowBlock({
id,
data,
selected,
}: NodeProps<WorkflowBlockProps>) {
const { type, config, name, isPending } = data
@@ -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)

View File

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

View File

@@ -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
}
/**
@@ -32,11 +34,13 @@ export function getBlockRingStyles(options: BlockRingOptions): {
diffStatus,
runPathStatus,
isPreviewSelection,
isSelected,
} = options
const hasRing =
isExecuting ||
isEditorOpen ||
isSelected ||
isPending ||
diffStatus === 'new' ||
diffStatus === 'edited' ||
@@ -46,25 +50,37 @@ 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' &&
@@ -72,6 +88,7 @@ export function getBlockRingStyles(options: BlockRingOptions): {
// Run path states (lowest priority - only show if no other states active)
!isExecuting &&
!isEditorOpen &&
!isSelected &&
!isPending &&
!isDeletedBlock &&
!diffStatus &&
@@ -79,6 +96,7 @@ export function getBlockRingStyles(options: BlockRingOptions): {
'ring-[var(--border-success)]',
!isExecuting &&
!isEditorOpen &&
!isSelected &&
!isPending &&
!isDeletedBlock &&
!diffStatus &&

View File

@@ -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]

View File

@@ -406,21 +406,13 @@ 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)
})
@@ -428,21 +420,15 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
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)
})