refactor(ui): eliminate prop drilling in editor, home, sidebar, and logs dashboard (#4950)

* refactor(ui): eliminate prop drilling in editor, home, sidebar, and logs dashboard

- panel editor: activeSearchTarget was relayed untouched through SubBlock and
  30+ input components (depth up to 6); now provided once via
  ActiveSearchTargetProvider and re-provided at tool-input's synthetic
  sub-block transformation points; removed the vestigial workspaceId
  option from SubBlockInputController/useSubBlockInput
- home: ChatSurfaceProvider carries chatId/userId and stable interaction
  callbacks to UserInput/MessageContent/MessageActions;
  MothershipResourcesProvider carries the five resource operations to
  ResourceTabs, removing MothershipView's pure-relay props
- sidebar: extended SidebarDragContext into SidebarListContext so
  WorkflowItem/FolderItem read selection/drag callbacks directly; moved the
  hidden import input up to sidebar.tsx (also fixes import no-op while the
  list shows a skeleton)
- logs dashboard: DashboardSegmentsContext feeds StatusBar directly;
  WorkflowsList no longer relays segment selection state

* refactor(home): sync ChatSurfaceProvider callback refs in a layout effect

Mutating refs during render is unsound under concurrent rendering (render
may run multiple times before commit); useLayoutEffect commits the latest
callbacks before any user event can fire.
This commit is contained in:
Waleed
2026-06-10 14:01:38 -07:00
committed by GitHub
parent dd3705e9da
commit 3c22e1e2bb
66 changed files with 1229 additions and 1059 deletions
@@ -17,6 +17,7 @@ import {
toast,
} from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
import { useSubmitCopilotFeedback } from '@/hooks/queries/copilot-feedback'
import { useForkMothershipChat } from '@/hooks/queries/mothership-chats'
import { useFolderStore } from '@/stores/folders/store'
@@ -49,7 +50,6 @@ const BUTTON_CLASS =
interface MessageActionsProps {
content: string
chatId?: string
userQuery?: string
requestId?: string
messageId?: string
@@ -57,13 +57,13 @@ interface MessageActionsProps {
export const MessageActions = memo(function MessageActions({
content,
chatId,
userQuery,
requestId,
messageId,
}: MessageActionsProps) {
const router = useRouter()
const params = useParams<{ workspaceId: string }>()
const { chatId } = useChatSurface()
const [copied, setCopied] = useState(false)
const [copiedRequestId, setCopiedRequestId] = useState(false)
const [pendingFeedback, setPendingFeedback] = useState<'up' | 'down' | null>(null)
@@ -0,0 +1,106 @@
'use client'
import {
createContext,
type ReactNode,
useCallback,
useContext,
useLayoutEffect,
useMemo,
useRef,
} from 'react'
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
import type { ChatContext } from '@/stores/panel'
/**
* Identity and interaction callbacks shared across a Mothership chat surface
* (home conversation view, home initial view, copilot panel). Carried via
* context so leaf components (UserInput, MessageContent, MessageActions) can
* consume them without relaying through every intermediate component.
*/
interface ChatSurfaceContextValue {
/** Resolved id of the chat backing this surface, if one exists yet. */
chatId?: string
/** Id of the user interacting with this surface. */
userId?: string
/** Notifies the surface owner that a context chip was added to the input. */
onContextAdd: (context: ChatContext) => void
/** Notifies the surface owner that a context chip was removed from the input. */
onContextRemove: (context: ChatContext) => void
/** Opens a workspace resource referenced from rendered message content. */
onWorkspaceResourceSelect: (resource: MothershipResource) => void
}
const noop = () => {}
const ChatSurfaceContext = createContext<ChatSurfaceContextValue>({
onContextAdd: noop,
onContextRemove: noop,
onWorkspaceResourceSelect: noop,
})
interface ChatSurfaceProviderProps {
chatId?: string
userId?: string
onContextAdd?: (context: ChatContext) => void
onContextRemove?: (context: ChatContext) => void
onWorkspaceResourceSelect?: (resource: MothershipResource) => void
children: ReactNode
}
/**
* Provides the chat-surface identity and interaction callbacks to descendants.
* Callbacks are latched in refs and exposed as stable wrappers so the memoized
* context value only changes when `chatId` or `userId` change — consumers do
* not re-render when a parent re-creates a handler.
*/
export function ChatSurfaceProvider({
chatId,
userId,
onContextAdd,
onContextRemove,
onWorkspaceResourceSelect,
children,
}: ChatSurfaceProviderProps) {
const onContextAddRef = useRef(onContextAdd)
const onContextRemoveRef = useRef(onContextRemove)
const onWorkspaceResourceSelectRef = useRef(onWorkspaceResourceSelect)
useLayoutEffect(() => {
onContextAddRef.current = onContextAdd
onContextRemoveRef.current = onContextRemove
onWorkspaceResourceSelectRef.current = onWorkspaceResourceSelect
})
const stableOnContextAdd = useCallback((context: ChatContext) => {
onContextAddRef.current?.(context)
}, [])
const stableOnContextRemove = useCallback((context: ChatContext) => {
onContextRemoveRef.current?.(context)
}, [])
const stableOnWorkspaceResourceSelect = useCallback((resource: MothershipResource) => {
onWorkspaceResourceSelectRef.current?.(resource)
}, [])
const value = useMemo<ChatSurfaceContextValue>(
() => ({
chatId,
userId,
onContextAdd: stableOnContextAdd,
onContextRemove: stableOnContextRemove,
onWorkspaceResourceSelect: stableOnWorkspaceResourceSelect,
}),
[chatId, userId, stableOnContextAdd, stableOnContextRemove, stableOnWorkspaceResourceSelect]
)
return <ChatSurfaceContext.Provider value={value}>{children}</ChatSurfaceContext.Provider>
}
/**
* Reads the surrounding chat surface. Outside a provider this returns no-op
* callbacks and undefined identity, matching the previous optional-prop
* behavior.
*/
export function useChatSurface(): ChatSurfaceContextValue {
return useContext(ChatSurfaceContext)
}
@@ -0,0 +1 @@
export { ChatSurfaceProvider, useChatSurface } from './chat-surface-context'
@@ -1,4 +1,5 @@
export { ChatMessageAttachments } from './chat-message-attachments'
export { ChatSurfaceProvider, useChatSurface } from './chat-surface-context'
export { ContextMentionIcon } from './context-mention-icon'
export { CreditsChip } from './credits-chip'
export {
@@ -6,6 +7,10 @@ export {
MessageContent,
} from './message-content'
export { MothershipChat } from './mothership-chat'
export {
MothershipResourcesProvider,
useMothershipResources,
} from './mothership-resources-context'
export { MothershipView } from './mothership-view'
export { QueuedMessages } from './queued-messages'
export { SuggestedActions } from './suggested-actions'
@@ -6,7 +6,8 @@ import { Read as ReadTool, WorkspaceFile } from '@/lib/copilot/generated/tool-ca
import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools'
import { resolveToolDisplay } from '@/lib/copilot/tools/client/store-utils'
import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-call-state'
import type { ContentBlock, MothershipResource, OptionItem, ToolCallData } from '../../types'
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
import type { ContentBlock, OptionItem, ToolCallData } from '../../types'
import { SUBAGENT_LABELS, TOOL_UI_METADATA } from '../../types'
import type { AgentGroupItem } from './components'
import {
@@ -676,7 +677,6 @@ interface MessageContentProps {
fallbackContent: string
isStreaming: boolean
onOptionSelect?: (id: string) => void
onWorkspaceResourceSelect?: (resource: MothershipResource) => void
}
function MessageContentInner({
@@ -684,8 +684,8 @@ function MessageContentInner({
fallbackContent,
isStreaming = false,
onOptionSelect,
onWorkspaceResourceSelect,
}: MessageContentProps) {
const { onWorkspaceResourceSelect } = useChatSurface()
const parsed = useMemo(() => (blocks.length > 0 ? parseBlocks(blocks) : []), [blocks])
const segments: MessageSegment[] =
@@ -4,6 +4,7 @@ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef } from '
import { cn } from '@/lib/core/utils/cn'
import { MessageActions } from '@/app/workspace/[workspaceId]/components'
import { ChatMessageAttachments } from '@/app/workspace/[workspaceId]/home/components/chat-message-attachments'
import { ChatSurfaceProvider } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
import {
assistantMessageHasRenderableContent,
MessageContent,
@@ -124,20 +125,16 @@ interface AssistantMessageRowProps {
message: ChatMessage
isStreaming: boolean
precedingUserContent?: string
chatId?: string
rowClassName: string
onOptionSelect?: (id: string) => void
onWorkspaceResourceSelect?: (resource: MothershipResource) => void
}
const AssistantMessageRow = memo(function AssistantMessageRow({
message,
isStreaming,
precedingUserContent,
chatId,
rowClassName,
onOptionSelect,
onWorkspaceResourceSelect,
}: AssistantMessageRowProps) {
const blocks = message.contentBlocks ?? EMPTY_BLOCKS
const hasAnyBlocks = blocks.length > 0
@@ -161,13 +158,11 @@ const AssistantMessageRow = memo(function AssistantMessageRow({
fallbackContent={message.content}
isStreaming={isStreaming}
onOptionSelect={onOptionSelect}
onWorkspaceResourceSelect={onWorkspaceResourceSelect}
/>
{showActions && (
<div className='mt-2.5'>
<MessageActions
content={message.content}
chatId={chatId}
userQuery={precedingUserContent}
requestId={message.requestId}
messageId={message.id}
@@ -241,17 +236,12 @@ export function MothershipChat({
const userInputRef = useRef<UserInputHandle>(null)
const onSubmitRef = useRef(onSubmit)
const onWorkspaceResourceSelectRef = useRef(onWorkspaceResourceSelect)
useEffect(() => {
onSubmitRef.current = onSubmit
onWorkspaceResourceSelectRef.current = onWorkspaceResourceSelect
}, [onSubmit, onWorkspaceResourceSelect])
}, [onSubmit])
const stableOnOptionSelect = useCallback((id: string) => {
onSubmitRef.current(id)
}, [])
const stableOnWorkspaceResourceSelect = useCallback((resource: MothershipResource) => {
onWorkspaceResourceSelectRef.current?.(resource)
}, [])
function handleSendQueuedHead() {
const topMessage = messageQueue[0]
@@ -286,75 +276,78 @@ export function MothershipChat({
}, [isStaging, stagedMessageCount, initialScrollBlocked, scrollToBottom])
return (
<div className={cn('flex h-full min-h-0 flex-col', className)}>
<div ref={scrollContainerRef} className={styles.scrollContainer}>
{isLoading && !hasMessages ? (
<MothershipChatSkeleton layout={layout} />
) : (
<div className={styles.content}>
{stagedMessages.map((msg, localIndex) => {
const index = stagedOffset + localIndex
if (msg.role === 'user') {
<ChatSurfaceProvider
chatId={chatId}
userId={userId}
onContextAdd={onContextAdd}
onContextRemove={onContextRemove}
onWorkspaceResourceSelect={onWorkspaceResourceSelect}
>
<div className={cn('flex h-full min-h-0 flex-col', className)}>
<div ref={scrollContainerRef} className={styles.scrollContainer}>
{isLoading && !hasMessages ? (
<MothershipChatSkeleton layout={layout} />
) : (
<div className={styles.content}>
{stagedMessages.map((msg, localIndex) => {
const index = stagedOffset + localIndex
if (msg.role === 'user') {
return (
<UserMessageRow
key={msg.id}
content={msg.content}
contexts={msg.contexts}
attachments={msg.attachments}
rowClassName={styles.userRow}
bubbleClassName={styles.userBubble}
attachmentWidthClassName={styles.attachmentWidth}
/>
)
}
const isLast = index === messages.length - 1
return (
<UserMessageRow
key={msg.id}
content={msg.content}
contexts={msg.contexts}
attachments={msg.attachments}
rowClassName={styles.userRow}
bubbleClassName={styles.userBubble}
attachmentWidthClassName={styles.attachmentWidth}
<AssistantMessageRow
key={assistantTurnKeyByIndex[index] ?? msg.id}
message={msg}
isStreaming={isStreamActive && isLast}
precedingUserContent={precedingUserContentByIndex[index]}
rowClassName={styles.assistantRow}
onOptionSelect={isLast ? stableOnOptionSelect : undefined}
/>
)
}
})}
</div>
)}
</div>
const isLast = index === messages.length - 1
return (
<AssistantMessageRow
key={assistantTurnKeyByIndex[index] ?? msg.id}
message={msg}
isStreaming={isStreamActive && isLast}
precedingUserContent={precedingUserContentByIndex[index]}
chatId={chatId}
rowClassName={styles.assistantRow}
onOptionSelect={isLast ? stableOnOptionSelect : undefined}
onWorkspaceResourceSelect={stableOnWorkspaceResourceSelect}
/>
)
})}
<div
className={cn(styles.footer, animateInput && 'animate-slide-in-bottom')}
onAnimationEnd={animateInput ? onInputAnimationEnd : undefined}
>
<div className={styles.footerInner}>
<QueuedMessages
messageQueue={messageQueue}
editingQueuedId={editingQueuedId}
dispatchingHeadId={dispatchingHeadId}
onRemove={onRemoveQueuedMessage}
onSendNow={onSendQueuedMessage}
onEdit={handleEditQueued}
onCancelEdit={onCancelQueueEdit}
/>
<UserInput
ref={userInputRef}
onSubmit={onSubmit}
isSending={isStreamActive}
onStopGeneration={onStopGeneration}
isInitialView={false}
onSendQueuedHead={handleSendQueuedHead}
onEditQueuedTail={handleEditQueuedTail}
draftScopeKey={draftScopeKey}
/>
</div>
)}
</div>
<div
className={cn(styles.footer, animateInput && 'animate-slide-in-bottom')}
onAnimationEnd={animateInput ? onInputAnimationEnd : undefined}
>
<div className={styles.footerInner}>
<QueuedMessages
messageQueue={messageQueue}
editingQueuedId={editingQueuedId}
dispatchingHeadId={dispatchingHeadId}
onRemove={onRemoveQueuedMessage}
onSendNow={onSendQueuedMessage}
onEdit={handleEditQueued}
onCancelEdit={onCancelQueueEdit}
/>
<UserInput
ref={userInputRef}
onSubmit={onSubmit}
isSending={isStreamActive}
onStopGeneration={onStopGeneration}
isInitialView={false}
userId={userId}
onContextAdd={onContextAdd}
onContextRemove={onContextRemove}
onSendQueuedHead={handleSendQueuedHead}
onEditQueuedTail={handleEditQueuedTail}
draftScopeKey={draftScopeKey}
/>
</div>
</div>
</div>
</ChatSurfaceProvider>
)
}
@@ -0,0 +1,4 @@
export {
MothershipResourcesProvider,
useMothershipResources,
} from './mothership-resources-context'
@@ -0,0 +1,69 @@
'use client'
import { createContext, type ReactNode, useContext, useMemo } from 'react'
import type {
MothershipResource,
MothershipResourceType,
} from '@/app/workspace/[workspaceId]/home/types'
/**
* Resource-management operations for the Mothership resource panel. Provided
* by the home surface (which owns the resource state via `useChat`) and
* consumed at the leaves (`ResourceTabs`, `MothershipView`) so the operations
* do not have to be relayed through intermediate components.
*/
interface MothershipResourcesContextValue {
/** Makes the given resource the active tab. */
selectResource: (id: string) => void
/** Adds a resource to the panel and activates it. */
addResource: (resource: MothershipResource) => void
/** Removes a resource from the panel. */
removeResource: (resourceType: MothershipResourceType, resourceId: string) => void
/** Replaces the resource list with a new ordering. */
reorderResources: (resources: MothershipResource[]) => void
/** Collapses the resource panel. */
collapseResource: () => void
}
const MothershipResourcesContext = createContext<MothershipResourcesContextValue | null>(null)
interface MothershipResourcesProviderProps extends MothershipResourcesContextValue {
children: ReactNode
}
/**
* Provides resource-management operations to the resource panel subtree. All
* operations are expected to be referentially stable; the context value is
* memoized on their identities.
*/
export function MothershipResourcesProvider({
selectResource,
addResource,
removeResource,
reorderResources,
collapseResource,
children,
}: MothershipResourcesProviderProps) {
const value = useMemo<MothershipResourcesContextValue>(
() => ({ selectResource, addResource, removeResource, reorderResources, collapseResource }),
[selectResource, addResource, removeResource, reorderResources, collapseResource]
)
return (
<MothershipResourcesContext.Provider value={value}>
{children}
</MothershipResourcesContext.Provider>
)
}
/**
* Reads the resource-management operations for the surrounding resource panel.
* Must be called under a {@link MothershipResourcesProvider}.
*/
export function useMothershipResources(): MothershipResourcesContextValue {
const value = useContext(MothershipResourcesContext)
if (!value) {
throw new Error('useMothershipResources must be used within a MothershipResourcesProvider')
}
return value
}
@@ -16,6 +16,7 @@ import { SIM_RESOURCE_DRAG_TYPE, SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/r
import { isEphemeralResource } from '@/lib/copilot/resources/types'
import { cn } from '@/lib/core/utils/cn'
import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer'
import { useMothershipResources } from '@/app/workspace/[workspaceId]/home/components/mothership-resources-context'
import { AddResourceDropdown } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown'
import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
import {
@@ -241,11 +242,6 @@ interface ResourceTabsProps {
chatId?: string
resources: MothershipResource[]
activeId: string | null
onSelect: (id: string) => void
onAddResource: (resource: MothershipResource) => void
onRemoveResource: (resourceType: MothershipResourceType, resourceId: string) => void
onReorderResources: (resources: MothershipResource[]) => void
onCollapse: () => void
previewMode?: PreviewMode
onCyclePreviewMode?: () => void
actions?: ReactNode
@@ -256,17 +252,19 @@ export function ResourceTabs({
chatId,
resources,
activeId,
onSelect,
onAddResource,
onRemoveResource,
onReorderResources,
onCollapse,
previewMode,
onCyclePreviewMode,
actions,
}: ResourceTabsProps) {
const PreviewModeIcon = PREVIEW_MODE_ICONS[previewMode ?? 'split']
const nameLookup = useResourceNameLookup(workspaceId)
const {
selectResource,
addResource: onAddResource,
removeResource: onRemoveResource,
reorderResources: onReorderResources,
collapseResource,
} = useMothershipResources()
const scrollNodeRef = useRef<HTMLDivElement>(null)
useEffect(() => {
@@ -354,7 +352,7 @@ export function ResourceTabs({
const next = new Set<string>()
for (let i = start; i <= end; i++) next.add(resources[i].id)
setSelectedIds(next)
onSelect(resource.id)
selectResource(resource.id)
return
}
}
@@ -370,11 +368,11 @@ export function ResourceTabs({
if (activeId === resource.id) {
const fallback =
findNearestId(resources, idx, next) ?? findNearestId(resources, idx, null)
if (fallback) onSelect(fallback)
if (fallback) selectResource(fallback)
}
} else {
setSelectedIds((prev) => new Set(prev).add(resource.id))
onSelect(resource.id)
selectResource(resource.id)
}
if (!anchorIdRef.current) anchorIdRef.current = resource.id
return
@@ -383,9 +381,9 @@ export function ResourceTabs({
// Plain click: single-select
anchorIdRef.current = resource.id
setSelectedIds(new Set([resource.id]))
onSelect(resource.id)
selectResource(resource.id)
},
[resources, onSelect, selectedIds, activeId]
[resources, selectResource, selectedIds, activeId]
)
const handleRemove = useCallback(
@@ -553,7 +551,7 @@ export function ResourceTabs({
<Tooltip.Trigger asChild>
<Button
variant='subtle'
onClick={onCollapse}
onClick={collapseResource}
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
aria-label='Collapse resource view'
>
@@ -623,7 +621,7 @@ export function ResourceTabs({
workspaceId={workspaceId}
existingKeys={existingKeys}
onAdd={handleAdd}
onSwitch={onSelect}
onSwitch={selectResource}
excludeTypes={ADD_RESOURCE_EXCLUDED_TYPES}
/>
)}
@@ -6,11 +6,11 @@ import { cn } from '@/lib/core/utils/cn'
import { getFileExtension } from '@/lib/uploads/utils/file-utils'
import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer'
import { RICH_PREVIEWABLE_EXTENSIONS } from '@/app/workspace/[workspaceId]/files/components/file-viewer'
import { useMothershipResources } from '@/app/workspace/[workspaceId]/home/components/mothership-resources-context'
import { hasRenderableFilePreviewContent } from '@/app/workspace/[workspaceId]/home/hooks/preview'
import type {
GenericResourceData,
MothershipResource,
MothershipResourceType,
} from '@/app/workspace/[workspaceId]/home/types'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { ResourceActions, ResourceContent, ResourceTabs } from './components'
@@ -44,11 +44,6 @@ interface MothershipViewProps {
chatId?: string
resources: MothershipResource[]
activeResourceId: string | null
onSelectResource: (id: string) => void
onAddResource: (resource: MothershipResource) => void
onRemoveResource: (resourceType: MothershipResourceType, resourceId: string) => void
onReorderResources: (resources: MothershipResource[]) => void
onCollapse: () => void
isCollapsed: boolean
className?: string
previewSession?: FilePreviewSession | null
@@ -62,11 +57,6 @@ export const MothershipView = memo(
chatId,
resources,
activeResourceId,
onSelectResource,
onAddResource,
onRemoveResource,
onReorderResources,
onCollapse,
isCollapsed,
className,
previewSession,
@@ -76,6 +66,7 @@ export const MothershipView = memo(
) {
const active = resources.find((r) => r.id === activeResourceId) ?? resources[0] ?? null
const { canEdit } = useUserPermissionsContext()
const { removeResource } = useMothershipResources()
const previewForActive =
previewSession && active && shouldShowStreamingFilePanel(previewSession, active)
@@ -111,11 +102,6 @@ export const MothershipView = memo(
chatId={chatId}
resources={resources}
activeId={active?.id ?? null}
onSelect={onSelectResource}
onAddResource={onAddResource}
onRemoveResource={onRemoveResource}
onReorderResources={onReorderResources}
onCollapse={onCollapse}
actions={
active ? <ResourceActions workspaceId={workspaceId} resource={active} /> : null
}
@@ -131,7 +117,7 @@ export const MothershipView = memo(
previewSession={previewForActive}
genericResourceData={active.type === 'generic' ? genericResourceData : undefined}
previewContextKey={chatId}
onNotFound={(resourceId) => onRemoveResource('log', resourceId)}
onNotFound={(resourceId) => removeResource('log', resourceId)}
/>
) : (
<div className='flex h-full items-center justify-center text-[var(--text-muted)] text-sm'>
@@ -18,6 +18,7 @@ import { getMothershipAttachmentPreviewUrl } from '@/lib/copilot/chat/attachment
import { SIM_RESOURCE_DRAG_TYPE, SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types'
import { cn } from '@/lib/core/utils/cn'
import { CHAT_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon'
import { useAvailableResources } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown'
import { snapSelectionToChips } from '@/app/workspace/[workspaceId]/home/components/user-input/chip-selection'
@@ -130,9 +131,6 @@ interface UserInputProps {
isSending: boolean
onStopGeneration: () => void
isInitialView?: boolean
userId?: string
onContextAdd?: (context: ChatContext) => void
onContextRemove?: (context: ChatContext) => void
onSendQueuedHead?: () => void
onEditQueuedTail?: () => void
}
@@ -155,9 +153,6 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
isSending,
onStopGeneration,
isInitialView = true,
userId,
onContextAdd,
onContextRemove,
onSendQueuedHead,
onEditQueuedTail,
},
@@ -166,6 +161,7 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
const { workspaceId } = useParams<{ workspaceId: string }>()
const { navigateToSettings } = useSettingsNavigation()
const { data: skills = [] } = useSkills(workspaceId)
const { userId, onContextAdd, onContextRemove } = useChatSurface()
const [value, setValue] = useState(() => {
if (defaultValue) return defaultValue
if (!draftScopeKey) return ''
@@ -35,8 +35,10 @@ import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
import { useOAuthReturnRouter } from '@/hooks/use-oauth-return'
import type { ChatContext } from '@/stores/panel'
import {
ChatSurfaceProvider,
CreditsChip,
MothershipChat,
MothershipResourcesProvider,
MothershipView,
SuggestedActions,
UserInput,
@@ -378,17 +380,20 @@ export function Home({ chatId, userName, userId, initialResourceId = null }: Hom
What should we get done{firstName ? `, ${firstName}` : ''}?
</h1>
<div ref={initialViewInputRef} className='relative w-full max-w-[48rem]'>
<UserInput
ref={initialViewUserInputRef}
defaultValue={initialPrompt}
draftScopeKey={draftScopeKey}
onSubmit={handleSubmit}
isSending={isSending}
onStopGeneration={handleStopGeneration}
<ChatSurfaceProvider
userId={userId}
onContextAdd={handleContextAdd}
onContextRemove={handleInitialContextRemove}
/>
>
<UserInput
ref={initialViewUserInputRef}
defaultValue={initialPrompt}
draftScopeKey={draftScopeKey}
onSubmit={handleSubmit}
isSending={isSending}
onStopGeneration={handleStopGeneration}
/>
</ChatSurfaceProvider>
{/* Anchored out of flow so expanding/collapsing never shifts the centered input */}
<div className='absolute inset-x-0 top-full'>
<SuggestedActions
@@ -442,22 +447,25 @@ export function Home({ chatId, userName, userId, initialResourceId = null }: Hom
</div>
)}
<MothershipView
ref={mothershipRef}
workspaceId={workspaceId}
chatId={resolvedChatId}
resources={resources}
activeResourceId={activeResourceId}
onSelectResource={setActiveResourceId}
onAddResource={addResource}
onRemoveResource={removeResource}
onReorderResources={reorderResources}
onCollapse={collapseResource}
isCollapsed={isResourceCollapsed}
previewSession={previewSession}
genericResourceData={genericResourceData ?? undefined}
className={skipResourceTransition ? '!transition-none' : undefined}
/>
<MothershipResourcesProvider
selectResource={setActiveResourceId}
addResource={addResource}
removeResource={removeResource}
reorderResources={reorderResources}
collapseResource={collapseResource}
>
<MothershipView
ref={mothershipRef}
workspaceId={workspaceId}
chatId={resolvedChatId}
resources={resources}
activeResourceId={activeResourceId}
isCollapsed={isResourceCollapsed}
previewSession={previewSession}
genericResourceData={genericResourceData ?? undefined}
className={skipResourceTransition ? '!transition-none' : undefined}
/>
</MothershipResourcesProvider>
{isResourceCollapsed && (
<div className='absolute top-[8.5px] right-[16px]'>
@@ -1,5 +1,9 @@
import { memo, useMemo, useState } from 'react'
import { handleKeyboardActivation } from '@/lib/core/utils/keyboard'
import {
type SegmentSelectionMode,
useDashboardSegments,
} from '@/app/workspace/[workspaceId]/logs/components/dashboard/dashboard-segments-context'
export interface StatusBarSegment {
successRate: number
@@ -9,6 +13,20 @@ export interface StatusBarSegment {
timestamp: string
}
interface StatusBarInnerProps {
segments: StatusBarSegment[]
selectedSegmentIndices: number[] | null
onSegmentClick: (
workflowId: string,
index: number,
timestamp: string,
mode: SegmentSelectionMode
) => void
workflowId: string
segmentDurationMs: number
preferBelow?: boolean
}
function StatusBarInner({
segments,
selectedSegmentIndices,
@@ -16,19 +34,7 @@ function StatusBarInner({
workflowId,
segmentDurationMs,
preferBelow = false,
}: {
segments: StatusBarSegment[]
selectedSegmentIndices: number[] | null
onSegmentClick: (
workflowId: string,
index: number,
timestamp: string,
mode: 'single' | 'toggle' | 'range'
) => void
workflowId: string
segmentDurationMs: number
preferBelow?: boolean
}) {
}: StatusBarInnerProps) {
const [hoverIndex, setHoverIndex] = useState<number | null>(null)
const labels = useMemo(() => {
@@ -137,17 +143,15 @@ function StatusBarInner({
}
/**
* Custom equality function for StatusBar memo.
* Custom equality function for the memoized StatusBar body.
* Performs structural comparison of segments array to avoid re-renders
* when poll data returns new object references with identical content.
*/
function areStatusBarPropsEqual(
prev: Parameters<typeof StatusBarInner>[0],
next: Parameters<typeof StatusBarInner>[0]
): boolean {
function areStatusBarPropsEqual(prev: StatusBarInnerProps, next: StatusBarInnerProps): boolean {
if (prev.workflowId !== next.workflowId) return false
if (prev.segmentDurationMs !== next.segmentDurationMs) return false
if (prev.preferBelow !== next.preferBelow) return false
if (prev.onSegmentClick !== next.onSegmentClick) return false
if (prev.selectedSegmentIndices !== next.selectedSegmentIndices) {
if (!prev.selectedSegmentIndices || !next.selectedSegmentIndices) return false
@@ -177,4 +181,30 @@ function areStatusBarPropsEqual(
return true
}
export const StatusBar = memo(StatusBarInner, areStatusBarPropsEqual)
const MemoizedStatusBar = memo(StatusBarInner, areStatusBarPropsEqual)
export interface StatusBarProps {
segments: StatusBarSegment[]
workflowId: string
preferBelow?: boolean
}
/**
* Status bar for a single workflow row. Reads segment selection state from
* DashboardSegmentsContext and delegates to a structurally-memoized body so
* only bars whose selection actually changed re-render.
*/
export function StatusBar({ segments, workflowId, preferBelow = false }: StatusBarProps) {
const { selectedSegments, onSegmentClick, segmentDurationMs } = useDashboardSegments()
return (
<MemoizedStatusBar
segments={segments}
selectedSegmentIndices={selectedSegments[workflowId] || null}
onSegmentClick={onSegmentClick}
workflowId={workflowId}
segmentDurationMs={segmentDurationMs}
preferBelow={preferBelow}
/>
)
}
@@ -13,28 +13,19 @@ export interface WorkflowExecutionItem {
overallSuccessRate: number
}
interface WorkflowsListProps {
filteredExecutions: WorkflowExecutionItem[]
expandedWorkflowId: string | null
onToggleWorkflow: (workflowId: string) => void
searchQuery: string
}
function WorkflowsListInner({
filteredExecutions,
expandedWorkflowId,
onToggleWorkflow,
selectedSegments,
onSegmentClick,
searchQuery,
segmentDurationMs,
}: {
filteredExecutions: WorkflowExecutionItem[]
expandedWorkflowId: string | null
onToggleWorkflow: (workflowId: string) => void
selectedSegments: Record<string, number[]>
onSegmentClick: (
workflowId: string,
segmentIndex: number,
timestamp: string,
mode: 'single' | 'toggle' | 'range'
) => void
searchQuery: string
segmentDurationMs: number
}) {
}: WorkflowsListProps) {
return (
<div className='flex h-full flex-col overflow-hidden rounded-md bg-[var(--surface-2)] dark:bg-[var(--surface-1)]'>
{/* Table header */}
@@ -99,10 +90,7 @@ function WorkflowsListInner({
<div className='flex-1'>
<StatusBar
segments={workflow.segments}
selectedSegmentIndices={selectedSegments[workflow.workflowId] || null}
onSegmentClick={onSegmentClick}
workflowId={workflow.workflowId}
segmentDurationMs={segmentDurationMs}
preferBelow={idx < 2}
/>
</div>
@@ -0,0 +1,41 @@
'use client'
import { createContext, useContext } from 'react'
/**
* Selection mode for a segment click: plain click, cmd/ctrl+click, or shift+click.
*/
export type SegmentSelectionMode = 'single' | 'toggle' | 'range'
export interface DashboardSegmentsContextValue {
/** Selected segment indices keyed by workflow id. */
selectedSegments: Record<string, number[]>
/** Handles a segment click for selecting time segments. */
onSegmentClick: (
workflowId: string,
segmentIndex: number,
timestamp: string,
mode: SegmentSelectionMode
) => void
/** Duration of a single segment in milliseconds. */
segmentDurationMs: number
}
/**
* Feature-local context for dashboard segment selection state, provided by
* the dashboard and consumed by StatusBar without threading props through
* intermediate components.
*/
export const DashboardSegmentsContext = createContext<DashboardSegmentsContextValue | null>(null)
/**
* Returns the dashboard segment selection context.
* @throws Error when used outside DashboardSegmentsContext.Provider
*/
export function useDashboardSegments(): DashboardSegmentsContextValue {
const context = useContext(DashboardSegmentsContext)
if (!context) {
throw new Error('useDashboardSegments must be used within DashboardSegmentsContext.Provider')
}
return context
}
@@ -4,6 +4,10 @@ import { memo, useCallback, useMemo, useRef, useState } from 'react'
import { useParams } from 'next/navigation'
import { useShallow } from 'zustand/react/shallow'
import { Loader } from '@/components/emcn'
import {
DashboardSegmentsContext,
type SegmentSelectionMode,
} from '@/app/workspace/[workspaceId]/logs/components/dashboard/dashboard-segments-context'
import { formatLatency } from '@/app/workspace/[workspaceId]/logs/utils'
import type { DashboardStatsResponse, WorkflowStats } from '@/hooks/queries/logs'
import { useWorkflows } from '@/hooks/queries/workflows'
@@ -302,12 +306,7 @@ function DashboardInner({ stats, isLoading, error }: DashboardProps) {
* @param mode - Selection mode: 'single', 'toggle' (cmd+click), or 'range' (shift+click)
*/
const handleSegmentClick = useCallback(
(
workflowId: string,
segmentIndex: number,
_timestamp: string,
mode: 'single' | 'toggle' | 'range'
) => {
(workflowId: string, segmentIndex: number, _timestamp: string, mode: SegmentSelectionMode) => {
if (mode === 'toggle') {
setSelectedSegments((prev) => {
const currentSegments = prev[workflowId] || []
@@ -355,6 +354,15 @@ function DashboardInner({ stats, isLoading, error }: DashboardProps) {
[]
)
const segmentsContextValue = useMemo(
() => ({
selectedSegments,
onSegmentClick: handleSegmentClick,
segmentDurationMs: segmentMs,
}),
[selectedSegments, handleSegmentClick, segmentMs]
)
const resetKey = `${JSON.stringify(stats?.workflows?.map((w) => w.workflowId))}-${timeRange}-${workflowIds.join(',')}-${searchQuery}`
const prevResetKeyRef = useRef(resetKey)
if (resetKey !== prevResetKeyRef.current) {
@@ -477,15 +485,14 @@ function DashboardInner({ stats, isLoading, error }: DashboardProps) {
</div>
<div className='min-h-0 flex-1 overflow-hidden'>
<WorkflowsList
filteredExecutions={filteredExecutions as WorkflowExecution[]}
expandedWorkflowId={expandedWorkflowId}
onToggleWorkflow={handleToggleWorkflow}
selectedSegments={selectedSegments}
onSegmentClick={handleSegmentClick}
searchQuery={searchQuery}
segmentDurationMs={segmentMs}
/>
<DashboardSegmentsContext.Provider value={segmentsContextValue}>
<WorkflowsList
filteredExecutions={filteredExecutions as WorkflowExecution[]}
expandedWorkflowId={expandedWorkflowId}
onToggleWorkflow={handleToggleWorkflow}
searchQuery={searchQuery}
/>
</DashboardSegmentsContext.Provider>
</div>
</div>
)
@@ -3,7 +3,7 @@ import { Checkbox, Label, Tooltip } from '@/components/emcn'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
interface CheckboxListOption {
label: string
@@ -19,7 +19,6 @@ interface CheckboxListProps {
isPreview?: boolean
subBlockValues?: Record<string, any>
disabled?: boolean
activeSearchTarget?: ActiveSearchTarget | null
}
interface CheckboxItemProps {
@@ -30,7 +29,6 @@ interface CheckboxItemProps {
isPreview: boolean
subBlockValues?: Record<string, any>
disabled: boolean
activeSearchTarget?: ActiveSearchTarget | null
}
/**
@@ -49,8 +47,8 @@ function CheckboxItem({
isPreview,
subBlockValues,
disabled,
activeSearchTarget,
}: CheckboxItemProps) {
const activeSearchTarget = useActiveSearchTarget()
const [storeValue, setStoreValue] = useSubBlockValue<boolean>(blockId, option.id)
const workflowSearchHighlight = getWorkflowSearchLabelHighlight({
activeSearchTarget,
@@ -105,7 +103,6 @@ export function CheckboxList({
isPreview = false,
subBlockValues,
disabled = false,
activeSearchTarget,
}: CheckboxListProps) {
return (
<div className='flex flex-col gap-y-2.5 pt-1'>
@@ -119,7 +116,6 @@ export function CheckboxList({
isPreview={isPreview}
subBlockValues={subBlockValues}
disabled={disabled}
activeSearchTarget={activeSearchTarget}
/>
))}
</div>
@@ -38,6 +38,7 @@ import {
import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { restoreCursorAfterInsertion } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/utils'
import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
@@ -48,7 +49,6 @@ import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/re
import { useTagSelection } from '@/hooks/kb/use-tag-selection'
import { createShouldHighlightEnvVar, useAvailableEnvVarKeys } from '@/hooks/use-available-env-vars'
import { useCodeUndoRedo } from '@/hooks/use-code-undo-redo'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
const logger = createLogger('Code')
@@ -213,7 +213,6 @@ interface CodeProps {
wandControlRef?: React.MutableRefObject<WandControlHandlers | null>
/** Whether to hide the internal wand button (controlled by parent) */
hideInternalWand?: boolean
activeSearchTarget?: ActiveSearchTarget | null
}
export const Code = memo(function Code({
@@ -233,8 +232,8 @@ export const Code = memo(function Code({
wandConfig,
wandControlRef,
hideInternalWand = false,
activeSearchTarget,
}: CodeProps) {
const activeSearchTarget = useActiveSearchTarget()
const params = useParams()
const workspaceId = params.workspaceId as string
@@ -10,13 +10,13 @@ import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/
import { SubBlockInputController } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sub-block-input-controller'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { getBlock } from '@/blocks/registry'
import type { SubBlockConfig } from '@/blocks/types'
import { getDependsOnFields } from '@/blocks/utils'
import { usePermissionConfig } from '@/hooks/use-permission-config'
import { getProviderFromModel } from '@/providers/utils'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -70,7 +70,6 @@ interface ComboBoxProps {
) => Promise<{ label: string; id: string } | null>
/** Field dependencies that trigger option refetch when changed */
dependsOn?: SubBlockConfig['dependsOn']
activeSearchTarget?: ActiveSearchTarget | null
}
export const ComboBox = memo(function ComboBox({
@@ -87,8 +86,8 @@ export const ComboBox = memo(function ComboBox({
fetchOptions,
fetchOptionById,
dependsOn,
activeSearchTarget,
}: ComboBoxProps) {
const activeSearchTarget = useActiveSearchTarget()
// Hooks and context
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlockId)
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
@@ -37,13 +37,13 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { restoreCursorAfterInsertion } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/utils'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { normalizeName } from '@/executor/constants'
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
import { useTagSelection } from '@/hooks/kb/use-tag-selection'
import { createShouldHighlightEnvVar, useAvailableEnvVarKeys } from '@/hooks/use-available-env-vars'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
const logger = createLogger('ConditionInput')
@@ -105,7 +105,6 @@ interface ConditionInputProps {
disabled?: boolean
/** Mode: 'condition' for code editor, 'router' for text input */
mode?: 'condition' | 'router'
activeSearchTarget?: ActiveSearchTarget | null
}
/**
@@ -134,8 +133,8 @@ export function ConditionInput({
previewValue,
disabled = false,
mode = 'condition',
activeSearchTarget,
}: ConditionInputProps) {
const activeSearchTarget = useActiveSearchTarget()
const isRouterMode = mode === 'router'
const params = useParams()
const workspaceId = params.workspaceId as string
@@ -21,6 +21,7 @@ import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { getBareIconStyle, type StyleableIcon } from '@/blocks/icon-color'
import type { SubBlockConfig } from '@/blocks/types'
import { CREDENTIAL_SET } from '@/executor/constants'
@@ -30,7 +31,6 @@ import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials'
import { useOrganizations } from '@/hooks/queries/organization'
import { useSubscriptionData } from '@/hooks/queries/subscription'
import { useCredentialRefreshTriggers } from '@/hooks/use-credential-refresh-triggers'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
interface CredentialSelectorProps {
@@ -40,7 +40,6 @@ interface CredentialSelectorProps {
isPreview?: boolean
previewValue?: any | null
previewContextValues?: Record<string, unknown>
activeSearchTarget?: ActiveSearchTarget | null
}
export function CredentialSelector({
@@ -50,8 +49,8 @@ export function CredentialSelector({
isPreview = false,
previewValue,
previewContextValues,
activeSearchTarget,
}: CredentialSelectorProps) {
const activeSearchTarget = useActiveSearchTarget()
const params = useParams()
const workspaceId = (params?.workspaceId as string) || ''
const [showConnectModal, setShowConnectModal] = useState(false)
@@ -21,11 +21,11 @@ import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import type { SubBlockConfig } from '@/blocks/types'
import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions'
import { useTagSelection } from '@/hooks/kb/use-tag-selection'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
interface DocumentTag {
id: string
@@ -43,7 +43,6 @@ interface DocumentTagEntryProps {
isPreview?: boolean
previewValue?: any
previewContextValues?: Record<string, unknown>
activeSearchTarget?: ActiveSearchTarget | null
}
/**
@@ -64,8 +63,8 @@ export function DocumentTagEntry({
isPreview = false,
previewValue,
previewContextValues,
activeSearchTarget,
}: DocumentTagEntryProps) {
const activeSearchTarget = useActiveSearchTarget()
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlock.id)
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
const valueInputRefs = useRef<Record<string, HTMLInputElement>>({})
@@ -8,11 +8,11 @@ import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/sub
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { getBlock } from '@/blocks/registry'
import type { SubBlockConfig } from '@/blocks/types'
import { getDependsOnFields } from '@/blocks/utils'
import { ResponseBlockHandler } from '@/executor/handlers/response/response-handler'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -69,7 +69,6 @@ interface DropdownProps {
dependsOn?: SubBlockConfig['dependsOn']
/** Enable search input in dropdown */
searchable?: boolean
activeSearchTarget?: ActiveSearchTarget | null
}
/**
@@ -96,8 +95,8 @@ export const Dropdown = memo(function Dropdown({
fetchOptionById,
dependsOn,
searchable = false,
activeSearchTarget,
}: DropdownProps) {
const activeSearchTarget = useActiveSearchTarget()
const [storeValue, setStoreValue] = useSubBlockValue<string | string[]>(blockId, subBlockId) as [
string | string[] | null | undefined,
(value: string | string[]) => void,
@@ -9,8 +9,8 @@ import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/compon
import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
interface EvalMetric {
id: string
@@ -28,7 +28,6 @@ interface EvalInputProps {
isPreview?: boolean
previewValue?: EvalMetric[] | null
disabled?: boolean
activeSearchTarget?: ActiveSearchTarget | null
}
// Default values
@@ -45,8 +44,8 @@ export function EvalInput({
isPreview = false,
previewValue,
disabled = false,
activeSearchTarget,
}: EvalInputProps) {
const activeSearchTarget = useActiveSearchTarget()
const [storeValue, setStoreValue] = useSubBlockValue<EvalMetric[]>(blockId, subBlockId)
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
const descriptionInputRefs = useRef<Record<string, HTMLTextAreaElement>>({})
@@ -18,12 +18,12 @@ import { getExtensionFromMimeType } from '@/lib/uploads/utils/file-utils'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import {
useUploadWorkspaceFile,
useWorkspaceFiles,
workspaceFilesKeys,
} from '@/hooks/queries/workspace-files'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -38,7 +38,6 @@ interface FileUploadProps {
isPreview?: boolean
previewValue?: any | null
disabled?: boolean
activeSearchTarget?: ActiveSearchTarget | null
}
interface UploadedFile {
@@ -165,8 +164,8 @@ export function FileUpload({
isPreview = false,
previewValue,
disabled = false,
activeSearchTarget,
}: FileUploadProps) {
const activeSearchTarget = useActiveSearchTarget()
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([])
const [uploadProgress, setUploadProgress] = useState(0)
@@ -19,8 +19,8 @@ import {
getWorkflowSearchLabelHighlight,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import type { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
interface FilterRuleRowProps {
blockId: string
@@ -38,7 +38,6 @@ interface FilterRuleRowProps {
onUpdate: (id: string, field: keyof FilterRule, value: string) => void
onToggleCollapse: (id: string) => void
inputController: ReturnType<typeof useSubBlockInput>
activeSearchTarget?: ActiveSearchTarget | null
}
export function FilterRuleRow({
@@ -55,8 +54,8 @@ export function FilterRuleRow({
onUpdate,
onToggleCollapse,
inputController,
activeSearchTarget,
}: FilterRuleRowProps) {
const activeSearchTarget = useActiveSearchTarget()
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
const valueInputRef = useRef<HTMLInputElement>(null)
const overlayRef = useRef<HTMLDivElement>(null)
@@ -9,7 +9,7 @@ import { useFilterBuilder } from '@/lib/table/query-builder/use-query-builder'
import { useCanonicalSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-canonical-sub-block-value'
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { FilterRuleRow } from './components/filter-rule-row'
interface FilterBuilderProps {
@@ -20,7 +20,6 @@ interface FilterBuilderProps {
disabled?: boolean
columns?: Array<{ value: string; label: string }>
tableIdSubBlockId?: string
activeSearchTarget?: ActiveSearchTarget | null
}
const createDefaultRule = (columns: ComboboxOption[]): FilterRule => ({
@@ -41,8 +40,8 @@ export function FilterBuilder({
disabled = false,
columns: propColumns,
tableIdSubBlockId = 'tableId',
activeSearchTarget,
}: FilterBuilderProps) {
const activeSearchTarget = useActiveSearchTarget()
const [storeValue, setStoreValue] = useSubBlockValue<FilterRule[]>(blockId, subBlockId)
const tableIdValue = useCanonicalSubBlockValue<string>(blockId, tableIdSubBlockId)
@@ -120,7 +119,6 @@ export function FilterBuilder({
onUpdate={updateRule}
onToggleCollapse={toggleCollapse}
inputController={inputController}
activeSearchTarget={activeSearchTarget}
/>
)
})}
@@ -14,7 +14,7 @@ import { cn } from '@/lib/core/utils/cn'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
interface SelectedCountDisplayProps {
noneSelected: boolean
@@ -49,7 +49,6 @@ interface GroupedCheckboxListProps {
subBlockValues: Record<string, any>
disabled?: boolean
maxHeight?: number
activeSearchTarget?: ActiveSearchTarget | null
}
export function GroupedCheckboxList({
@@ -61,8 +60,8 @@ export function GroupedCheckboxList({
subBlockValues,
disabled = false,
maxHeight = 400,
activeSearchTarget,
}: GroupedCheckboxListProps) {
const activeSearchTarget = useActiveSearchTarget()
const [open, setOpen] = useState(false)
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
const optionRefs = useRef<Record<number, HTMLDivElement | null>>({})
@@ -8,10 +8,10 @@ import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import type { SubBlockConfig } from '@/blocks/types'
import { useWorkflowState } from '@/hooks/queries/workflows'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
/**
* Props for the InputMappingField component
@@ -43,7 +43,6 @@ interface InputMappingProps {
disabled?: boolean
/** Sub-block values from the preview context for resolving sibling sub-block values */
previewContextValues?: Record<string, unknown>
activeSearchTarget?: ActiveSearchTarget | null
}
/**
@@ -58,8 +57,8 @@ export function InputMapping({
previewValue,
disabled = false,
previewContextValues,
activeSearchTarget,
}: InputMappingProps) {
const activeSearchTarget = useActiveSearchTarget()
const subBlockId = subBlock.id
const [mapping, setMapping] = useSubBlockValue(blockId, subBlockId)
const { dependencyValues } = useDependsOnGate(blockId, subBlock, {
@@ -10,10 +10,10 @@ import type { KnowledgeBaseData } from '@/lib/knowledge/types'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import type { SubBlockConfig } from '@/blocks/types'
import { useKnowledgeBasesList } from '@/hooks/kb/use-knowledge'
import { fetchKnowledgeBase, knowledgeKeys } from '@/hooks/queries/kb/knowledge'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
interface KnowledgeBaseSelectorProps {
blockId: string
@@ -22,7 +22,6 @@ interface KnowledgeBaseSelectorProps {
onKnowledgeBaseSelect?: (knowledgeBaseId: string | string[]) => void
isPreview?: boolean
previewValue?: string | null
activeSearchTarget?: ActiveSearchTarget | null
}
export function KnowledgeBaseSelector({
@@ -32,8 +31,8 @@ export function KnowledgeBaseSelector({
onKnowledgeBaseSelect,
isPreview = false,
previewValue,
activeSearchTarget,
}: KnowledgeBaseSelectorProps) {
const activeSearchTarget = useActiveSearchTarget()
const params = useParams()
const workspaceId = params.workspaceId as string
@@ -21,11 +21,11 @@ import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/compon
import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import type { SubBlockConfig } from '@/blocks/types'
import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions'
import { useTagSelection } from '@/hooks/kb/use-tag-selection'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useSubBlockValue } from '../../hooks/use-sub-block-value'
interface TagFilter {
@@ -46,7 +46,6 @@ interface KnowledgeTagFiltersProps {
isPreview?: boolean
previewValue?: string | null
previewContextValues?: Record<string, unknown>
activeSearchTarget?: ActiveSearchTarget | null
}
/**
@@ -68,8 +67,8 @@ export function KnowledgeTagFilters({
isPreview = false,
previewValue,
previewContextValues,
activeSearchTarget,
}: KnowledgeTagFiltersProps) {
const activeSearchTarget = useActiveSearchTarget()
const [storeValue, setStoreValue] = useSubBlockValue<string | null>(blockId, subBlock.id)
const emitTagSelection = useTagSelection(blockId, subBlock.id)
const valueInputRefs = useRef<Record<string, HTMLInputElement>>({})
@@ -18,11 +18,11 @@ import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand'
import type { SubBlockConfig } from '@/blocks/types'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
const logger = createLogger('LongInput')
@@ -69,7 +69,6 @@ interface LongInputProps {
wandControlRef?: React.MutableRefObject<WandControlHandlers | null>
/** Whether to hide the internal wand button (controlled by parent) */
hideInternalWand?: boolean
activeSearchTarget?: ActiveSearchTarget | null
workflowSearchValuePath?: Array<string | number>
}
@@ -96,9 +95,9 @@ export function LongInput({
disabled,
wandControlRef,
hideInternalWand = false,
activeSearchTarget,
workflowSearchValuePath = [],
}: LongInputProps) {
const activeSearchTarget = useActiveSearchTarget()
// Local state for immediate UI updates during streaming
const [localContent, setLocalContent] = useState<string>('')
const persistSubBlockValueRef = useRef<(value: string) => void>(() => {})
@@ -8,9 +8,9 @@ import { ShortInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/compone
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import type { SubBlockConfig } from '@/blocks/types'
import { useMcpTools } from '@/hooks/mcp/use-mcp-tools'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { formatParameterLabel } from '@/tools/params'
const logger = createLogger('McpDynamicArgs')
@@ -22,7 +22,6 @@ interface McpDynamicArgsProps {
isPreview?: boolean
previewValue?: any
previewContextValues?: Record<string, unknown>
activeSearchTarget?: ActiveSearchTarget | null
}
/**
@@ -53,8 +52,8 @@ export function McpDynamicArgs({
isPreview = false,
previewValue,
previewContextValues,
activeSearchTarget,
}: McpDynamicArgsProps) {
const activeSearchTarget = useActiveSearchTarget()
const params = useParams()
const workspaceId = params.workspaceId as string
const { mcpTools, isLoading } = useMcpTools(workspaceId)
@@ -254,7 +253,6 @@ export function McpDynamicArgs({
onChange={(newValue) => updateParameter(paramName, newValue)}
isPreview={isPreview}
disabled={disabled}
activeSearchTarget={activeSearchTarget}
workflowSearchValuePath={[paramName]}
/>
)
@@ -295,7 +293,6 @@ export function McpDynamicArgs({
}}
isPreview={isPreview}
disabled={disabled}
activeSearchTarget={activeSearchTarget}
workflowSearchValuePath={[paramName]}
/>
)
@@ -6,9 +6,9 @@ import { Combobox } from '@/components/emcn/components'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import type { SubBlockConfig } from '@/blocks/types'
import { useMcpServers } from '@/hooks/queries/mcp'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
interface McpServerSelectorProps {
blockId: string
@@ -16,7 +16,6 @@ interface McpServerSelectorProps {
disabled?: boolean
isPreview?: boolean
previewValue?: string | null
activeSearchTarget?: ActiveSearchTarget | null
}
export function McpServerSelector({
@@ -25,8 +24,8 @@ export function McpServerSelector({
disabled = false,
isPreview = false,
previewValue,
activeSearchTarget,
}: McpServerSelectorProps) {
const activeSearchTarget = useActiveSearchTarget()
const params = useParams()
const workspaceId = params.workspaceId as string
const [inputValue, setInputValue] = useState('')
@@ -7,9 +7,9 @@ import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import type { SubBlockConfig } from '@/blocks/types'
import { useMcpTools } from '@/hooks/mcp/use-mcp-tools'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
interface McpToolSelectorProps {
blockId: string
@@ -18,7 +18,6 @@ interface McpToolSelectorProps {
isPreview?: boolean
previewValue?: string | null
previewContextValues?: Record<string, unknown>
activeSearchTarget?: ActiveSearchTarget | null
}
export function McpToolSelector({
@@ -28,8 +27,8 @@ export function McpToolSelector({
isPreview = false,
previewValue,
previewContextValues,
activeSearchTarget,
}: McpToolSelectorProps) {
const activeSearchTarget = useActiveSearchTarget()
const params = useParams()
const workspaceId = params.workspaceId as string
const [inputValue, setInputValue] = useState('')
@@ -20,10 +20,10 @@ import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand'
import type { SubBlockConfig } from '@/blocks/types'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
const MIN_TEXTAREA_HEIGHT_PX = 80
const MAX_TEXTAREA_HEIGHT_PX = 320
@@ -70,7 +70,6 @@ interface MessagesInputProps {
disabled?: boolean
/** Ref to expose wand control handlers to parent */
wandControlRef?: React.MutableRefObject<WandControlHandlers | null>
activeSearchTarget?: ActiveSearchTarget | null
}
/**
@@ -89,8 +88,8 @@ export function MessagesInput({
previewValue,
disabled = false,
wandControlRef,
activeSearchTarget,
}: MessagesInputProps) {
const activeSearchTarget = useActiveSearchTarget()
const [messages, setMessages] = useSubBlockValue<Message[]>(blockId, subBlockId, false)
const [localMessages, setLocalMessages] = useState<Message[]>([{ role: 'user', content: '' }])
const messageIdsRef = useRef<string[]>([generateShortId()])
@@ -1,5 +1,4 @@
import { ResponseFormat as SharedResponseFormat } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
interface ResponseFormatProps {
blockId: string
@@ -8,7 +7,6 @@ interface ResponseFormatProps {
previewValue?: any
disabled?: boolean
config?: any
activeSearchTarget?: ActiveSearchTarget | null
}
export function ResponseFormat({
@@ -18,7 +16,6 @@ export function ResponseFormat({
previewValue,
disabled = false,
config,
activeSearchTarget,
}: ResponseFormatProps) {
return (
<SharedResponseFormat
@@ -28,7 +25,6 @@ export function ResponseFormat({
previewValue={previewValue}
disabled={disabled}
config={config}
activeSearchTarget={activeSearchTarget}
/>
)
}
@@ -6,6 +6,7 @@ import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/
import { SubBlockInputController } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sub-block-input-controller'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import type { SubBlockConfig } from '@/blocks/types'
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
import {
@@ -13,7 +14,6 @@ import {
useSelectorOptionMap,
useSelectorOptions,
} from '@/hooks/selectors/use-selector-query'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
interface SelectorComboboxProps {
blockId: string
@@ -28,7 +28,6 @@ interface SelectorComboboxProps {
onOptionChange?: (value: string) => void
allowSearch?: boolean
missingOptionLabel?: string
activeSearchTarget?: ActiveSearchTarget | null
}
export function SelectorCombobox({
@@ -44,8 +43,8 @@ export function SelectorCombobox({
onOptionChange,
allowSearch = true,
missingOptionLabel,
activeSearchTarget,
}: SelectorComboboxProps) {
const activeSearchTarget = useActiveSearchTarget()
const [storeValueRaw, setStoreValue] = useSubBlockValue<string | null | undefined>(
blockId,
subBlock.id
@@ -8,7 +8,6 @@ import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
import type { SubBlockConfig } from '@/blocks/types'
import type { SelectorContext } from '@/hooks/selectors/types'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
export interface SelectorOverrides {
transformContext?: (context: SelectorContext, deps: Record<string, unknown>) => SelectorContext
@@ -23,7 +22,6 @@ interface SelectorInputProps {
previewValue?: any
previewContextValues?: Record<string, any>
overrides?: SelectorOverrides
activeSearchTarget?: ActiveSearchTarget | null
}
export function SelectorInput({
@@ -34,7 +32,6 @@ export function SelectorInput({
previewValue,
previewContextValues,
overrides,
activeSearchTarget,
}: SelectorInputProps) {
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
const [storeValue] = useSubBlockValue(blockId, subBlock.id)
@@ -104,7 +101,6 @@ export function SelectorInput({
collaborativeSetSubblockValue(blockId, subBlock.id, value)
}
}}
activeSearchTarget={activeSearchTarget}
/>
)
}
@@ -12,12 +12,12 @@ import { SubBlockInputController } from '@/app/workspace/[workspaceId]/w/[workfl
import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand'
import type { SubBlockConfig } from '@/blocks/types'
import { useWebhookManagement } from '@/hooks/use-webhook-management'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
/**
* Props for the ShortInput component
@@ -53,7 +53,6 @@ interface ShortInputProps {
hideInternalWand?: boolean
/** Whether workflow search is actively highlighting this input */
isSearchHighlighted?: boolean
activeSearchTarget?: ActiveSearchTarget | null
workflowSearchValuePath?: Array<string | number>
}
@@ -84,9 +83,9 @@ export const ShortInput = memo(function ShortInput({
wandControlRef,
hideInternalWand = false,
isSearchHighlighted = false,
activeSearchTarget,
workflowSearchValuePath = [],
}: ShortInputProps) {
const activeSearchTarget = useActiveSearchTarget()
const [localContent, setLocalContent] = useState<string>('')
const [isFocused, setIsFocused] = useState(false)
const persistSubBlockValueRef = useRef<(value: string) => void>(() => {})
@@ -9,10 +9,10 @@ import { SkillModal } from '@/app/workspace/[workspaceId]/skills/components/skil
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import type { SkillDefinition } from '@/hooks/queries/skills'
import { useSkills } from '@/hooks/queries/skills'
import { usePermissionConfig } from '@/hooks/use-permission-config'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
interface StoredSkill {
skillId: string
@@ -25,7 +25,6 @@ interface SkillInputProps {
isPreview?: boolean
previewValue?: unknown
disabled?: boolean
activeSearchTarget?: ActiveSearchTarget | null
}
export function SkillInput({
@@ -34,8 +33,8 @@ export function SkillInput({
isPreview,
previewValue,
disabled,
activeSearchTarget,
}: SkillInputProps) {
const activeSearchTarget = useActiveSearchTarget()
const params = useParams()
const workspaceId = params.workspaceId as string
@@ -5,7 +5,7 @@ import { handleKeyboardActivation } from '@/lib/core/utils/keyboard'
import type { SortRule } from '@/lib/table/query-builder/constants'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
interface SortRuleRowProps {
rule: SortRule
@@ -15,7 +15,6 @@ interface SortRuleRowProps {
isReadOnly: boolean
blockId: string
subBlockId: string
activeSearchTarget?: ActiveSearchTarget | null
onAdd: () => void
onRemove: (id: string) => void
onUpdate: (id: string, field: keyof SortRule, value: string) => void
@@ -30,12 +29,12 @@ export function SortRuleRow({
isReadOnly,
blockId,
subBlockId,
activeSearchTarget,
onAdd,
onRemove,
onUpdate,
onToggleCollapse,
}: SortRuleRowProps) {
const activeSearchTarget = useActiveSearchTarget()
const getDirectionLabel = (value: string) => {
const option = directionOptions.find((dir) => dir.value === value)
return option?.label || value
@@ -7,7 +7,6 @@ import { useTableColumns } from '@/lib/table/hooks'
import { SORT_DIRECTIONS, type SortRule } from '@/lib/table/query-builder/constants'
import { useCanonicalSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-canonical-sub-block-value'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { SortRuleRow } from './components/sort-rule-row'
interface SortBuilderProps {
@@ -18,7 +17,6 @@ interface SortBuilderProps {
disabled?: boolean
columns?: Array<{ value: string; label: string }>
tableIdSubBlockId?: string
activeSearchTarget?: ActiveSearchTarget | null
}
const createDefaultRule = (columns: ComboboxOption[]): SortRule => ({
@@ -37,7 +35,6 @@ export function SortBuilder({
disabled = false,
columns: propColumns,
tableIdSubBlockId = 'tableId',
activeSearchTarget,
}: SortBuilderProps) {
const [storeValue, setStoreValue] = useSubBlockValue<SortRule[]>(blockId, subBlockId)
const tableIdValue = useCanonicalSubBlockValue<string>(blockId, tableIdSubBlockId)
@@ -103,7 +100,6 @@ export function SortBuilder({
isReadOnly={isReadOnly}
blockId={blockId}
subBlockId={subBlockId}
activeSearchTarget={activeSearchTarget}
onAdd={addRule}
onRemove={removeRule}
onUpdate={updateRule}
@@ -26,8 +26,8 @@ import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/compon
import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
interface Field {
id: string
@@ -52,7 +52,6 @@ interface FieldFormatProps {
valuePlaceholder?: string
descriptionPlaceholder?: string
config?: any
activeSearchTarget?: ActiveSearchTarget | null
}
/**
@@ -107,8 +106,8 @@ export function FieldFormat({
showDescription = false,
valuePlaceholder = 'Enter default value',
descriptionPlaceholder = 'Describe this field',
activeSearchTarget,
}: FieldFormatProps) {
const activeSearchTarget = useActiveSearchTarget()
const [storeValue, setStoreValue] = useSubBlockValue<Field[]>(blockId, subBlockId)
const valueInputRefs = useRef<Record<string, HTMLInputElement | HTMLTextAreaElement>>({})
const nameInputRefs = useRef<Record<string, HTMLInputElement>>({})
@@ -33,8 +33,6 @@ export interface SubBlockInputControllerProps {
onStreamingEnd?: () => void
/** Optional preview value for read-only preview. */
previewValue?: string | null
/** Optional workspace id for env var scoping. */
workspaceId?: string
/**
* Optional callback to force/show the env var dropdown (e.g., API key fields).
* Return { show: true, searchTerm?: string } to override defaults.
@@ -84,7 +82,6 @@ export function SubBlockInputController(props: SubBlockInputControllerProps): Re
isStreaming,
onStreamingEnd,
previewValue,
workspaceId,
shouldForceEnvDropdown,
shouldForceTagDropdown,
children,
@@ -101,7 +98,6 @@ export function SubBlockInputController(props: SubBlockInputControllerProps): Re
isStreaming,
onStreamingEnd,
previewValue,
workspaceId,
shouldForceEnvDropdown,
shouldForceTagDropdown,
})
@@ -6,9 +6,9 @@ import { Combobox, type ComboboxOption } from '@/components/emcn'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import type { SubBlockConfig } from '@/blocks/types'
import { useTablesList } from '@/hooks/queries/tables'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
interface TableSelectorProps {
blockId: string
@@ -16,7 +16,6 @@ interface TableSelectorProps {
disabled?: boolean
isPreview?: boolean
previewValue?: string | null
activeSearchTarget?: ActiveSearchTarget | null
}
/**
@@ -33,8 +32,8 @@ export function TableSelector({
disabled = false,
isPreview = false,
previewValue,
activeSearchTarget,
}: TableSelectorProps) {
const activeSearchTarget = useActiveSearchTarget()
const params = useParams()
const workspaceId = params.workspaceId as string
@@ -14,8 +14,8 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
const logger = createLogger('Table')
@@ -26,7 +26,6 @@ interface TableProps {
isPreview?: boolean
previewValue?: WorkflowTableRow[] | null
disabled?: boolean
activeSearchTarget?: ActiveSearchTarget | null
}
interface WorkflowTableRow {
@@ -49,7 +48,6 @@ interface TableCellProps {
overlayRefs: React.MutableRefObject<Map<string, HTMLDivElement>>
accessiblePrefixes: ReturnType<typeof useAccessibleReferencePrefixes>
workspaceId: string
activeSearchTarget?: ActiveSearchTarget | null
subBlockId: string
}
@@ -68,9 +66,9 @@ function TableCell({
overlayRefs,
accessiblePrefixes,
workspaceId,
activeSearchTarget,
subBlockId,
}: TableCellProps) {
const activeSearchTarget = useActiveSearchTarget()
// Defensive programming: ensure row.cells exists and has the expected structure
const hasValidCells = row.cells && typeof row.cells === 'object'
if (!hasValidCells) logger.warn('Table row has malformed cells data:', row)
@@ -217,8 +215,8 @@ export function Table({
isPreview = false,
previewValue,
disabled = false,
activeSearchTarget,
}: TableProps) {
const activeSearchTarget = useActiveSearchTarget()
const params = useParams()
const workspaceId = params.workspaceId as string
const [storeValue, setStoreValue] = useSubBlockValue<WorkflowTableRow[]>(blockId, subBlockId)
@@ -399,7 +397,6 @@ export function Table({
overlayRefs={overlayRefs}
accessiblePrefixes={accessiblePrefixes}
workspaceId={workspaceId}
activeSearchTarget={activeSearchTarget}
subBlockId={subBlockId}
/>
))}
@@ -4,7 +4,7 @@ import { TimePicker } from '@/components/emcn'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
interface TimeInputProps {
blockId: string
@@ -14,7 +14,6 @@ interface TimeInputProps {
previewValue?: string | null
className?: string
disabled?: boolean
activeSearchTarget?: ActiveSearchTarget | null
}
function formatTimeInputDisplayLabel(value: string): string {
@@ -38,8 +37,8 @@ export function TimeInput({
previewValue,
className,
disabled = false,
activeSearchTarget,
}: TimeInputProps) {
const activeSearchTarget = useActiveSearchTarget()
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlockId)
const value = isPreview ? previewValue : storeValue
@@ -18,11 +18,11 @@ import { getMissingRequiredScopes } from '@/lib/oauth/utils'
import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { useWorkspaceCredential } from '@/hooks/queries/credentials'
import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials'
import { useWorkflowMap } from '@/hooks/queries/workflows'
import { useCredentialRefreshTriggers } from '@/hooks/use-credential-refresh-triggers'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
const getProviderIcon = (providerName: OAuthProvider) => {
@@ -64,7 +64,6 @@ interface ToolCredentialSelectorProps {
label?: string
serviceId: OAuthService
disabled?: boolean
activeSearchTarget?: ActiveSearchTarget | null
}
const EMPTY_SCOPES: string[] = []
@@ -79,8 +78,8 @@ export function ToolCredentialSelector({
label,
serviceId,
disabled = false,
activeSearchTarget,
}: ToolCredentialSelectorProps) {
const activeSearchTarget = useActiveSearchTarget()
const params = useParams()
const workspaceId = (params?.workspaceId as string) || ''
const onChangeRef = useRef(onChange)
@@ -8,7 +8,6 @@ import {
import { parseStoredToolInputValue } from '@/lib/workflows/tool-input/types'
import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -26,7 +25,6 @@ interface ToolSubBlockRendererProps {
disabled?: boolean
onToggle?: () => void
}
activeSearchTarget?: ActiveSearchTarget | null
}
/**
@@ -50,7 +48,6 @@ export function ToolSubBlockRenderer({
onParamChange,
disabled,
canonicalToggle,
activeSearchTarget,
}: ToolSubBlockRendererProps) {
const syntheticId = buildToolSubBlockId(subBlockId, toolIndex, effectiveParamId)
const toolParamValue = toolParams?.[effectiveParamId] ?? ''
@@ -128,7 +125,6 @@ export function ToolSubBlockRenderer({
disabled={disabled}
canonicalToggle={canonicalToggle}
dependencyContext={toolParams}
activeSearchTarget={activeSearchTarget}
/>
)
}
@@ -54,6 +54,10 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
import {
ActiveSearchTargetProvider,
useActiveSearchTarget,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { getAllBlocks } from '@/blocks'
import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
import { BUILT_IN_TOOL_TYPES } from '@/blocks/utils'
@@ -137,7 +141,6 @@ function WorkflowInputMapperInput({
onChange,
disabled,
workflowId,
activeSearchTarget,
}: {
blockId: string
paramId: string
@@ -145,8 +148,8 @@ function WorkflowInputMapperInput({
onChange: (value: string) => void
disabled: boolean
workflowId: string
activeSearchTarget?: ActiveSearchTarget | null
}) {
const activeSearchTarget = useActiveSearchTarget()
const { data: workflowState, isLoading } = useWorkflowState(workflowId)
const inputFields = useMemo(
() => (workflowState?.blocks ? extractInputFieldsFromBlocks(workflowState.blocks) : []),
@@ -207,21 +210,21 @@ function WorkflowInputMapperInput({
}
: null
return (
<ShortInput
key={field.name}
blockId={blockId}
subBlockId={syntheticId}
placeholder={`Enter ${field.name}${field.type !== 'string' ? ` (${field.type})` : ''}`}
value={String(parsedValue[field.name] ?? '')}
onChange={(newValue: string) => handleFieldChange(field.name, newValue)}
disabled={disabled}
activeSearchTarget={fieldActiveSearchTarget}
config={{
id: syntheticId,
type: 'short-input',
title: field.name,
}}
/>
<ActiveSearchTargetProvider key={field.name} value={fieldActiveSearchTarget}>
<ShortInput
blockId={blockId}
subBlockId={syntheticId}
placeholder={`Enter ${field.name}${field.type !== 'string' ? ` (${field.type})` : ''}`}
value={String(parsedValue[field.name] ?? '')}
onChange={(newValue: string) => handleFieldChange(field.name, newValue)}
disabled={disabled}
config={{
id: syntheticId,
type: 'short-input',
title: field.name,
}}
/>
</ActiveSearchTargetProvider>
)
})}
</div>
@@ -311,7 +314,6 @@ interface ToolInputProps {
disabled?: boolean
/** Allow expanding tools in preview mode */
allowExpandInPreview?: boolean
activeSearchTarget?: ActiveSearchTarget | null
}
/**
@@ -459,11 +461,11 @@ export const ToolInput = memo(function ToolInput({
previewValue,
disabled = false,
allowExpandInPreview,
activeSearchTarget,
}: ToolInputProps) {
const params = useParams()
const workspaceId = params.workspaceId as string
const workflowId = params.workflowId as string
const activeSearchTarget = useActiveSearchTarget()
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
const [open, setOpen] = useState(false)
const [customToolModalOpen, setCustomToolModalOpen] = useState(false)
@@ -1169,149 +1171,16 @@ export const ToolInput = memo(function ToolInput({
)
const uiComponent = param.uiComponent
if (!uiComponent) {
return (
<ShortInput
blockId={blockId}
subBlockId={uniqueSubBlockId}
placeholder={param.description || `Enter ${formatParameterLabel(param.id).toLowerCase()}`}
password={isPasswordParameter(param.id)}
config={{
id: uniqueSubBlockId,
type: 'short-input',
title: param.id,
}}
value={value}
onChange={onChange}
wandControlRef={wandControlRef}
hideInternalWand={true}
activeSearchTarget={paramActiveSearchTarget}
/>
)
}
switch (uiComponent.type) {
case 'dropdown': {
const options =
(uiComponent.options as { id?: string; label: string; value?: string }[] | undefined)
?.filter((option) => (option.id ?? option.value) !== '')
.map((option) => ({
label: option.label,
value: option.id ?? option.value ?? '',
})) || []
const selectedLabel = options.find((option) => option.value === value)?.label ?? ''
const workflowSearchHighlight = getWorkflowSearchLabelHighlight({
activeSearchTarget: paramActiveSearchTarget,
blockId,
subBlockId: uniqueSubBlockId,
valuePath: [],
label: selectedLabel,
})
const content = (() => {
if (!uiComponent) {
return (
<Combobox
options={options}
value={value}
onChange={onChange}
placeholder={uiComponent.placeholder || 'Select option'}
disabled={disabled}
overlayContent={
workflowSearchHighlight ? (
<span className='truncate text-[var(--text-primary)]'>
{formatDisplayText(selectedLabel, { workflowSearchHighlight })}
</span>
) : undefined
<ShortInput
blockId={blockId}
subBlockId={uniqueSubBlockId}
placeholder={
param.description || `Enter ${formatParameterLabel(param.id).toLowerCase()}`
}
/>
)
}
case 'switch':
return (
<Switch
checked={value === 'true' || value === 'True'}
onCheckedChange={(checked) => onChange(checked ? 'true' : 'false')}
/>
)
case 'long-input':
return (
<LongInput
blockId={blockId}
subBlockId={uniqueSubBlockId}
placeholder={uiComponent.placeholder || param.description}
config={{
id: uniqueSubBlockId,
type: 'long-input',
title: param.id,
wandConfig: uiComponent.wandConfig,
}}
value={value}
onChange={onChange}
wandControlRef={wandControlRef}
hideInternalWand={true}
activeSearchTarget={paramActiveSearchTarget}
/>
)
case 'short-input':
return (
<ShortInput
blockId={blockId}
subBlockId={uniqueSubBlockId}
placeholder={uiComponent.placeholder || param.description}
password={uiComponent.password || isPasswordParameter(param.id)}
config={{
id: uniqueSubBlockId,
type: 'short-input',
title: param.id,
wandConfig: uiComponent.wandConfig,
}}
value={value}
onChange={onChange}
disabled={disabled}
wandControlRef={wandControlRef}
hideInternalWand={true}
activeSearchTarget={paramActiveSearchTarget}
/>
)
case 'oauth-input':
return (
<ToolCredentialSelector
blockId={blockId}
subBlockId={uniqueSubBlockId}
value={value}
onChange={onChange}
provider={getProviderIdFromServiceId(uiComponent.serviceId || '') as OAuthProvider}
serviceId={uiComponent.serviceId as OAuthService}
disabled={disabled}
requiredScopes={uiComponent.requiredScopes || []}
activeSearchTarget={paramActiveSearchTarget}
/>
)
case 'workflow-input-mapper': {
const selectedWorkflowId = currentToolParams?.workflowId || ''
return (
<WorkflowInputMapperInput
blockId={blockId}
paramId={param.id}
value={value}
onChange={onChange}
disabled={disabled}
workflowId={selectedWorkflowId}
activeSearchTarget={paramActiveSearchTarget}
/>
)
}
default:
return (
<ShortInput
blockId={blockId}
subBlockId={uniqueSubBlockId}
placeholder={uiComponent.placeholder || param.description}
password={uiComponent.password || isPasswordParameter(param.id)}
password={isPasswordParameter(param.id)}
config={{
id: uniqueSubBlockId,
type: 'short-input',
@@ -1321,10 +1190,147 @@ export const ToolInput = memo(function ToolInput({
onChange={onChange}
wandControlRef={wandControlRef}
hideInternalWand={true}
activeSearchTarget={paramActiveSearchTarget}
/>
)
}
}
switch (uiComponent.type) {
case 'dropdown': {
const options =
(uiComponent.options as { id?: string; label: string; value?: string }[] | undefined)
?.filter((option) => (option.id ?? option.value) !== '')
.map((option) => ({
label: option.label,
value: option.id ?? option.value ?? '',
})) || []
const selectedLabel = options.find((option) => option.value === value)?.label ?? ''
const workflowSearchHighlight = getWorkflowSearchLabelHighlight({
activeSearchTarget: paramActiveSearchTarget,
blockId,
subBlockId: uniqueSubBlockId,
valuePath: [],
label: selectedLabel,
})
return (
<Combobox
options={options}
value={value}
onChange={onChange}
placeholder={uiComponent.placeholder || 'Select option'}
disabled={disabled}
overlayContent={
workflowSearchHighlight ? (
<span className='truncate text-[var(--text-primary)]'>
{formatDisplayText(selectedLabel, { workflowSearchHighlight })}
</span>
) : undefined
}
/>
)
}
case 'switch':
return (
<Switch
checked={value === 'true' || value === 'True'}
onCheckedChange={(checked) => onChange(checked ? 'true' : 'false')}
/>
)
case 'long-input':
return (
<LongInput
blockId={blockId}
subBlockId={uniqueSubBlockId}
placeholder={uiComponent.placeholder || param.description}
config={{
id: uniqueSubBlockId,
type: 'long-input',
title: param.id,
wandConfig: uiComponent.wandConfig,
}}
value={value}
onChange={onChange}
wandControlRef={wandControlRef}
hideInternalWand={true}
/>
)
case 'short-input':
return (
<ShortInput
blockId={blockId}
subBlockId={uniqueSubBlockId}
placeholder={uiComponent.placeholder || param.description}
password={uiComponent.password || isPasswordParameter(param.id)}
config={{
id: uniqueSubBlockId,
type: 'short-input',
title: param.id,
wandConfig: uiComponent.wandConfig,
}}
value={value}
onChange={onChange}
disabled={disabled}
wandControlRef={wandControlRef}
hideInternalWand={true}
/>
)
case 'oauth-input':
return (
<ToolCredentialSelector
blockId={blockId}
subBlockId={uniqueSubBlockId}
value={value}
onChange={onChange}
provider={getProviderIdFromServiceId(uiComponent.serviceId || '') as OAuthProvider}
serviceId={uiComponent.serviceId as OAuthService}
disabled={disabled}
requiredScopes={uiComponent.requiredScopes || []}
/>
)
case 'workflow-input-mapper': {
const selectedWorkflowId = currentToolParams?.workflowId || ''
return (
<WorkflowInputMapperInput
blockId={blockId}
paramId={param.id}
value={value}
onChange={onChange}
disabled={disabled}
workflowId={selectedWorkflowId}
/>
)
}
default:
return (
<ShortInput
blockId={blockId}
subBlockId={uniqueSubBlockId}
placeholder={uiComponent.placeholder || param.description}
password={uiComponent.password || isPasswordParameter(param.id)}
config={{
id: uniqueSubBlockId,
type: 'short-input',
title: param.id,
}}
value={value}
onChange={onChange}
wandControlRef={wandControlRef}
hideInternalWand={true}
/>
)
}
})()
return (
<ActiveSearchTargetProvider value={paramActiveSearchTarget}>
{content}
</ActiveSearchTargetProvider>
)
}
/**
@@ -2047,23 +2053,26 @@ export const ToolInput = memo(function ToolInput({
: { ...sb, title: formatParameterLabel(effectiveParamId) }
return (
<ToolSubBlockRenderer
<ActiveSearchTargetProvider
key={sb.id}
blockId={blockId}
subBlockId={subBlockId}
toolIndex={toolIndex}
subBlock={sbWithTitle}
effectiveParamId={effectiveParamId}
toolParams={tool.params}
onParamChange={handleParamChange}
disabled={disabled}
canonicalToggle={canonicalToggleProp}
activeSearchTarget={getParamActiveSearchTarget(
value={getParamActiveSearchTarget(
toolIndex,
effectiveParamId,
buildToolSubBlockId(subBlockId, toolIndex, effectiveParamId)
)}
/>
>
<ToolSubBlockRenderer
blockId={blockId}
subBlockId={subBlockId}
toolIndex={toolIndex}
subBlock={sbWithTitle}
effectiveParamId={effectiveParamId}
toolParams={tool.params}
onParamChange={handleParamChange}
disabled={disabled}
canonicalToggle={canonicalToggleProp}
/>
</ActiveSearchTargetProvider>
)
}
@@ -24,8 +24,8 @@ import {
getWorkflowSearchLabelHighlight,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useVariablesStore } from '@/stores/variables/store'
import type { Variable } from '@/stores/variables/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -45,7 +45,6 @@ interface VariablesInputProps {
isPreview?: boolean
previewValue?: VariableAssignment[] | null
disabled?: boolean
activeSearchTarget?: ActiveSearchTarget | null
}
const DEFAULT_ASSIGNMENT: Omit<VariableAssignment, 'id'> = {
@@ -90,8 +89,8 @@ export function VariablesInput({
isPreview = false,
previewValue,
disabled = false,
activeSearchTarget,
}: VariablesInputProps) {
const activeSearchTarget = useActiveSearchTarget()
const params = useParams()
const workflowId = params.workflowId as string
const [storeValue, setStoreValue] = useSubBlockValue<VariableAssignment[]>(blockId, subBlockId)
@@ -6,7 +6,6 @@ import { DELETED_WORKFLOW_LABEL } from '@/app/workspace/[workspaceId]/logs/utils
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
import type { SubBlockConfig } from '@/blocks/types'
import type { SelectorContext } from '@/hooks/selectors/types'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
interface WorkflowSelectorInputProps {
@@ -15,7 +14,6 @@ interface WorkflowSelectorInputProps {
disabled?: boolean
isPreview?: boolean
previewValue?: string | null
activeSearchTarget?: ActiveSearchTarget | null
}
export function WorkflowSelectorInput({
@@ -24,7 +22,6 @@ export function WorkflowSelectorInput({
disabled = false,
isPreview = false,
previewValue,
activeSearchTarget,
}: WorkflowSelectorInputProps) {
const { workspaceId } = useParams<{ workspaceId: string }>()
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
@@ -48,7 +45,6 @@ export function WorkflowSelectorInput({
previewValue={previewValue}
placeholder={subBlock.placeholder || 'Select workflow...'}
missingOptionLabel={DELETED_WORKFLOW_LABEL}
activeSearchTarget={activeSearchTarget}
/>
)
}
@@ -38,8 +38,6 @@ export interface UseSubBlockInputOptions {
onStreamingEnd?: () => void
/** Optional preview value for read-only preview displays. */
previewValue?: string | null
/** Optional workspace id; if omitted, derived from route params. */
workspaceId?: string
/**
* Optional callback to force/show the env var dropdown (e.g., API key fields).
* Return { show: true, searchTerm?: string } to override defaults.
@@ -162,13 +160,12 @@ export function useSubBlockInput(options: UseSubBlockInputOptions): UseSubBlockI
isStreaming = false,
onStreamingEnd,
previewValue,
workspaceId: workspaceIdProp,
shouldForceEnvDropdown,
shouldForceTagDropdown,
} = options
const params = useParams()
const workspaceId = (workspaceIdProp || (params?.workspaceId as string)) ?? undefined
const workspaceId = (params?.workspaceId as string) ?? undefined
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId, false, {
isStreaming,
@@ -54,7 +54,6 @@ import { MODAL_REGISTRY } from '@/app/workspace/[workspaceId]/w/[workflowId]/com
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import type { SubBlockConfig } from '@/blocks/types'
import { useWebhookManagement } from '@/hooks/use-webhook-management'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
const SLACK_OVERRIDES: SelectorOverrides = {
transformContext: (context, deps) => {
@@ -105,7 +104,6 @@ interface SubBlockProps {
/** Provides sibling values for dependency resolution in non-preview contexts (e.g. tool-input) */
dependencyContext?: Record<string, unknown>
isSearchHighlighted?: boolean
activeSearchTarget?: ActiveSearchTarget | null
}
/**
@@ -441,7 +439,6 @@ const arePropsEqual = (prevProps: SubBlockProps, nextProps: SubBlockProps): bool
canonicalToggleEqual &&
prevProps.labelSuffix === nextProps.labelSuffix &&
prevProps.isSearchHighlighted === nextProps.isSearchHighlighted &&
prevProps.activeSearchTarget === nextProps.activeSearchTarget &&
prevProps.dependencyContext === nextProps.dependencyContext
)
}
@@ -459,7 +456,6 @@ const arePropsEqual = (prevProps: SubBlockProps, nextProps: SubBlockProps): bool
* @param labelSuffix - Additional content rendered after the label text
* @param dependencyContext - Sibling values for dependency resolution in non-preview contexts (e.g. tool-input)
* @param isSearchHighlighted - Whether workflow search should highlight this field
* @param activeSearchTarget - Active workflow search target for nested field highlighting
*/
function SubBlockComponent({
blockId,
@@ -472,7 +468,6 @@ function SubBlockComponent({
labelSuffix,
dependencyContext,
isSearchHighlighted,
activeSearchTarget,
}: SubBlockProps): JSX.Element {
const params = useParams()
const workspaceId = params.workspaceId as string
@@ -655,7 +650,6 @@ function SubBlockComponent({
wandControlRef={wandControlRef}
hideInternalWand={true}
isSearchHighlighted={isSearchHighlighted}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -672,7 +666,6 @@ function SubBlockComponent({
disabled={isDisabled}
wandControlRef={wandControlRef}
hideInternalWand={true}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -693,7 +686,6 @@ function SubBlockComponent({
fetchOptionById={config.fetchOptionById}
dependsOn={config.dependsOn}
searchable={config.searchable}
activeSearchTarget={activeSearchTarget}
/>
</div>
)
@@ -707,7 +699,6 @@ function SubBlockComponent({
disabled={isDisabled}
isPreview={isPreview}
previewValue={previewValue as string | null}
activeSearchTarget={activeSearchTarget}
/>
</div>
)
@@ -728,7 +719,6 @@ function SubBlockComponent({
fetchOptions={config.fetchOptions}
fetchOptionById={config.fetchOptionById}
dependsOn={config.dependsOn}
activeSearchTarget={activeSearchTarget}
/>
</div>
)
@@ -758,7 +748,6 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue as any}
disabled={isDisabled}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -791,7 +780,6 @@ function SubBlockComponent({
}
wandControlRef={wandControlRef}
hideInternalWand={true}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -816,7 +804,6 @@ function SubBlockComponent({
previewValue={previewValue}
disabled={allowExpandInPreview ? false : isDisabled}
allowExpandInPreview={allowExpandInPreview}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -828,7 +815,6 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue}
disabled={isDisabled}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -848,7 +834,6 @@ function SubBlockComponent({
subBlockValues={subBlockValues}
disabled={isDisabled}
subBlockId={config.id}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -863,7 +848,6 @@ function SubBlockComponent({
subBlockValues={subBlockValues ?? {}}
disabled={isDisabled}
maxHeight={config.maxHeight}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -875,7 +859,6 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue as any}
disabled={isDisabled}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -888,7 +871,6 @@ function SubBlockComponent({
previewValue={previewValue as any}
disabled={isDisabled}
mode='router'
activeSearchTarget={activeSearchTarget}
/>
)
@@ -900,7 +882,6 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue as any}
disabled={isDisabled}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -913,7 +894,6 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue as any}
disabled={isDisabled}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -928,7 +908,6 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue as any}
disabled={isDisabled}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -944,7 +923,6 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue}
previewContextValues={contextValues}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -959,7 +937,6 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue}
previewContextValues={contextValues}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -973,7 +950,6 @@ function SubBlockComponent({
previewValue={previewValue}
previewContextValues={contextValues}
overrides={FOLDER_OVERRIDES}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -985,7 +961,6 @@ function SubBlockComponent({
disabled={isDisabled}
isPreview={isPreview}
previewValue={previewValue as any}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -998,7 +973,6 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue as any}
previewContextValues={contextValues}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -1011,7 +985,6 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue as any}
previewContextValues={contextValues}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -1024,7 +997,6 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue}
previewContextValues={contextValues}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -1038,7 +1010,6 @@ function SubBlockComponent({
disabled={isDisabled}
config={config}
showValue={true}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -1051,7 +1022,6 @@ function SubBlockComponent({
previewValue={previewValue as any}
disabled={isDisabled}
previewContextValues={contextValues}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -1063,7 +1033,6 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue as any}
disabled={isDisabled}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -1076,7 +1045,6 @@ function SubBlockComponent({
previewValue={previewValue}
config={config}
disabled={isDisabled}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -1088,7 +1056,6 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue as FilterRule[] | null | undefined}
disabled={isDisabled}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -1100,7 +1067,6 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue as SortRule[] | null | undefined}
disabled={isDisabled}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -1115,7 +1081,6 @@ function SubBlockComponent({
previewValue={previewValue}
previewContextValues={contextValues}
overrides={SLACK_OVERRIDES}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -1127,7 +1092,6 @@ function SubBlockComponent({
disabled={isDisabled}
isPreview={isPreview}
previewValue={previewValue as string | null}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -1139,7 +1103,6 @@ function SubBlockComponent({
disabled={isDisabled}
isPreview={isPreview}
previewValue={previewValue as any}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -1152,7 +1115,6 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue as any}
previewContextValues={contextValues}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -1165,7 +1127,6 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue}
previewContextValues={contextValues}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -1202,7 +1163,6 @@ function SubBlockComponent({
previewValue={previewValue as any}
disabled={isDisabled}
wandControlRef={wandControlRef}
activeSearchTarget={activeSearchTarget}
/>
)
@@ -10,7 +10,7 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import type { BlockState } from '@/stores/workflows/workflow/types'
import type { ConnectedBlock } from '../../hooks/use-block-connections'
import { useSubflowEditor } from '../../hooks/use-subflow-editor'
@@ -39,7 +39,6 @@ interface SubflowEditorProps {
toggleConnectionsCollapsed: () => void
userCanEdit: boolean
isConnectionsAtMinHeight: boolean
activeSearchTarget?: ActiveSearchTarget | null
}
/**
@@ -60,8 +59,8 @@ export function SubflowEditor({
toggleConnectionsCollapsed,
userCanEdit,
isConnectionsAtMinHeight,
activeSearchTarget,
}: SubflowEditorProps) {
const activeSearchTarget = useActiveSearchTarget()
const {
subflowConfig,
currentType,
@@ -39,6 +39,7 @@ import {
useEditorBlockProperties,
useEditorSubblockLayout,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks'
import { ActiveSearchTargetProvider } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config'
import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config'
import { getSubBlockStableKey } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/utils'
@@ -368,110 +369,111 @@ export function Editor() {
const isConnectionsAtMinHeight = connectionsHeight <= 35
return (
<div className='flex h-full flex-col'>
{/* Header */}
<div className='mx-[-1px] flex flex-shrink-0 items-center justify-between rounded-none border border-[var(--border)] bg-[var(--surface-4)] px-3 py-1.5'>
<div className='flex min-w-0 flex-1 items-center gap-2'>
{(blockConfig || isSubflow) && currentBlock?.type !== 'note' && (
<div
className='flex size-[18px] items-center justify-center rounded-sm'
style={{ background: isSubflow ? subflowConfig?.bgColor : blockConfig?.bgColor }}
>
<IconComponent
icon={isSubflow ? subflowConfig?.icon : blockConfig?.icon}
className='size-[12px] text-[var(--white)]'
<ActiveSearchTargetProvider value={activeSearchTargetForCurrentBlock}>
<div className='flex h-full flex-col'>
{/* Header */}
<div className='mx-[-1px] flex flex-shrink-0 items-center justify-between rounded-none border border-[var(--border)] bg-[var(--surface-4)] px-3 py-1.5'>
<div className='flex min-w-0 flex-1 items-center gap-2'>
{(blockConfig || isSubflow) && currentBlock?.type !== 'note' && (
<div
className='flex size-[18px] items-center justify-center rounded-sm'
style={{ background: isSubflow ? subflowConfig?.bgColor : blockConfig?.bgColor }}
>
<IconComponent
icon={isSubflow ? subflowConfig?.icon : blockConfig?.icon}
className='size-[12px] text-[var(--white)]'
/>
</div>
)}
{isRenaming ? (
<input
ref={nameInputRefCallback}
type='text'
value={editedName}
onChange={(e) => setEditedName(e.target.value)}
onBlur={handleSaveRename}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSaveRename()
} else if (e.key === 'Escape') {
handleCancelRename()
}
}}
className='min-w-0 flex-1 truncate bg-transparent pr-2 font-medium text-[var(--text-primary)] text-sm outline-none'
/>
</div>
)}
{isRenaming ? (
<input
ref={nameInputRefCallback}
type='text'
value={editedName}
onChange={(e) => setEditedName(e.target.value)}
onBlur={handleSaveRename}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSaveRename()
} else if (e.key === 'Escape') {
handleCancelRename()
}
}}
className='min-w-0 flex-1 truncate bg-transparent pr-2 font-medium text-[var(--text-primary)] text-sm outline-none'
/>
) : (
<h2
className='min-w-0 flex-1 cursor-pointer select-none text-ellipsis whitespace-nowrap pr-2 font-medium text-[var(--text-primary)] text-sm [overflow-clip-margin:3px] [overflow:clip]'
title={title}
onDoubleClick={handleStartRename}
onMouseDown={(e) => {
if (e.detail === 2) {
e.preventDefault()
}
}}
>
{blockNameSearchHighlight
? formatDisplayText(title, { workflowSearchHighlight: blockNameSearchHighlight })
: title}
</h2>
)}
</div>
<div className='flex shrink-0 items-center gap-2'>
{/* Locked indicator - clickable to unlock if user has admin permissions, block is locked directly, and not locked by an ancestor */}
{isLocked && currentBlock && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
{userPermissions.canAdmin && currentBlock.locked && !isAncestorLocked ? (
) : (
<h2
className='min-w-0 flex-1 cursor-pointer select-none text-ellipsis whitespace-nowrap pr-2 font-medium text-[var(--text-primary)] text-sm [overflow-clip-margin:3px] [overflow:clip]'
title={title}
onDoubleClick={handleStartRename}
onMouseDown={(e) => {
if (e.detail === 2) {
e.preventDefault()
}
}}
>
{blockNameSearchHighlight
? formatDisplayText(title, { workflowSearchHighlight: blockNameSearchHighlight })
: title}
</h2>
)}
</div>
<div className='flex shrink-0 items-center gap-2'>
{/* Locked indicator - clickable to unlock if user has admin permissions, block is locked directly, and not locked by an ancestor */}
{isLocked && currentBlock && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
{userPermissions.canAdmin && currentBlock.locked && !isAncestorLocked ? (
<Button
variant='ghost'
className='p-0'
onClick={() => collaborativeBatchToggleLocked([currentBlockId!])}
aria-label='Unlock block'
>
<Unlock className='size-[14px] text-[var(--text-secondary)]' />
</Button>
) : (
<div className='flex items-center justify-center'>
<Lock className='size-[14px] text-[var(--text-secondary)]' />
</div>
)}
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>
{isAncestorLocked
? 'Ancestor container is locked'
: userPermissions.canAdmin && currentBlock.locked
? 'Unlock block'
: 'Block is locked'}
</p>
</Tooltip.Content>
</Tooltip.Root>
)}
{/* Rename button */}
{currentBlock && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
className='p-0'
onClick={() => collaborativeBatchToggleLocked([currentBlockId!])}
aria-label='Unlock block'
onClick={isRenaming ? handleSaveRename : handleStartRename}
disabled={!canEditBlock}
aria-label={isRenaming ? 'Save name' : 'Rename block'}
>
<Unlock className='size-[14px] text-[var(--text-secondary)]' />
{isRenaming ? (
<Check className='size-[14px]' />
) : (
<Pencil className='size-[14px]' />
)}
</Button>
) : (
<div className='flex items-center justify-center'>
<Lock className='size-[14px] text-[var(--text-secondary)]' />
</div>
)}
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>
{isAncestorLocked
? 'Ancestor container is locked'
: userPermissions.canAdmin && currentBlock.locked
? 'Unlock block'
: 'Block is locked'}
</p>
</Tooltip.Content>
</Tooltip.Root>
)}
{/* Rename button */}
{currentBlock && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
className='p-0'
onClick={isRenaming ? handleSaveRename : handleStartRename}
disabled={!canEditBlock}
aria-label={isRenaming ? 'Save name' : 'Rename block'}
>
{isRenaming ? (
<Check className='size-[14px]' />
) : (
<Pencil className='size-[14px]' />
)}
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>{isRenaming ? 'Save name' : 'Rename block'}</p>
</Tooltip.Content>
</Tooltip.Root>
)}
{/* Focus on block button */}
{/* {currentBlock && (
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>{isRenaming ? 'Save name' : 'Rename block'}</p>
</Tooltip.Content>
</Tooltip.Root>
)}
{/* Focus on block button */}
{/* {currentBlock && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
@@ -488,291 +490,291 @@ export function Editor() {
</Tooltip.Content>
</Tooltip.Root>
)} */}
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
className='p-0'
onClick={handleOpenDocs}
aria-label='Open documentation'
>
<BookOpen className='size-[14px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>Open docs</p>
</Tooltip.Content>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
className='p-0'
onClick={handleOpenDocs}
aria-label='Open documentation'
>
<BookOpen className='size-[14px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>Open docs</p>
</Tooltip.Content>
</Tooltip.Root>
</div>
</div>
</div>
{!currentBlockId || !currentBlock ? (
<div className='flex flex-1 items-center justify-center text-[var(--text-placeholder)] text-small'>
Select a block to edit
</div>
) : isSubflow ? (
<SubflowEditor
currentBlock={currentBlock}
currentBlockId={currentBlockId}
subBlocksRef={subBlocksRef}
connectionsHeight={connectionsHeight}
isResizing={isResizing}
hasIncomingConnections={hasIncomingConnections}
incomingConnections={incomingConnections}
handleConnectionsResizeMouseDown={handleConnectionsResizeMouseDown}
toggleConnectionsCollapsed={toggleConnectionsCollapsed}
userCanEdit={canEditBlock}
isConnectionsAtMinHeight={isConnectionsAtMinHeight}
activeSearchTarget={activeSearchTargetForCurrentBlock}
/>
) : (
<div className='flex flex-1 flex-col overflow-hidden pt-[0px]'>
{/* Subblocks Section */}
<div
ref={subBlocksRef}
className='subblocks-section flex flex-1 flex-col overflow-hidden'
>
<div className='flex-1 overflow-y-auto overflow-x-hidden px-2 pt-3 pb-2 [overflow-anchor:none]'>
{/* Workflow Preview - only for workflow blocks with a selected child workflow */}
{isWorkflowBlock && childWorkflowId && (
<>
<div className='subblock-content flex flex-col gap-[9.5px]'>
<div className='pl-0.5 font-medium text-[var(--text-primary)] text-small leading-none'>
Workflow Preview
</div>
<div className='relative h-[160px] overflow-hidden rounded-sm border border-[var(--border)]'>
{isLoadingChildWorkflow ? (
<div className='flex h-full items-center justify-center bg-[var(--surface-3)]'>
<Loader className='size-5 text-[var(--text-tertiary)]' animate />
</div>
) : childWorkflowState ? (
<>
<div className='[&_*:active]:!cursor-grabbing [&_*]:!cursor-grab [&_.react-flow__handle]:!hidden h-full w-full'>
<PreviewWorkflow
workflowState={childWorkflowState}
height={160}
width='100%'
isPannable={true}
defaultZoom={0.6}
fitPadding={0.15}
cursorStyle='grab'
lightweight
/>
</div>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
type='button'
variant='ghost'
onClick={handleOpenChildWorkflow}
className='absolute right-[6px] bottom-1.5 z-10 size-[24px] cursor-pointer border border-[var(--border)] bg-[var(--surface-2)] p-0 hover-hover:bg-[var(--surface-4)]'
>
<ExternalLink className='size-[12px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>Open workflow</Tooltip.Content>
</Tooltip.Root>
</>
) : (
<div className='flex h-full items-center justify-center bg-[var(--surface-3)]'>
<span className='text-[var(--text-tertiary)] text-small'>
Unable to load preview
</span>
</div>
)}
</div>
</div>
<FieldDivider subblockMarker />
</>
)}
{subBlocks.length === 0 && !isWorkflowBlock ? (
<div className='flex h-full items-center justify-center text-center text-[var(--text-placeholder)] text-small'>
This block has no subblocks
</div>
) : (
<div className='flex flex-col'>
{regularSubBlocks.map((subBlock, index) => {
const stableKey = getSubBlockStableKey(
currentBlockId || '',
subBlock,
subBlockState
)
const canonicalId = canonicalIndex.canonicalIdBySubBlockId[subBlock.id]
const canonicalGroup = canonicalId
? canonicalIndex.groupsById[canonicalId]
: undefined
const isCanonicalSwap = isCanonicalPair(canonicalGroup)
const canonicalMode =
canonicalGroup && isCanonicalSwap
? resolveCanonicalMode(
canonicalGroup,
blockSubBlockValues,
canonicalModeOverrides
)
: undefined
const showDivider =
index < regularSubBlocks.length - 1 ||
(!hasAdvancedOnlyFields && index < subBlocks.length - 1)
return (
<div key={stableKey} className='subblock-row'>
<SubBlock
blockId={currentBlockId}
config={subBlock}
isPreview={false}
subBlockValues={subBlockState}
disabled={!canEditBlock}
allowExpandInPreview={false}
isSearchHighlighted={
activeSearchTarget?.blockId === currentBlockId &&
(activeSearchTarget.subBlockId === subBlock.id ||
activeSearchTarget.canonicalSubBlockId ===
(subBlock.canonicalParamId ?? subBlock.id))
}
activeSearchTarget={activeSearchTargetForCurrentBlock}
canonicalToggle={
isCanonicalSwap && canonicalMode && canonicalId
? {
mode: canonicalMode,
disabled: !canEditBlock,
onToggle: () => {
if (!currentBlockId) return
const nextMode =
canonicalMode === 'advanced' ? 'basic' : 'advanced'
collaborativeSetBlockCanonicalMode(
currentBlockId,
canonicalId,
nextMode
)
},
}
: undefined
}
/>
{showDivider && <FieldDivider subblockMarker />}
{!currentBlockId || !currentBlock ? (
<div className='flex flex-1 items-center justify-center text-[var(--text-placeholder)] text-small'>
Select a block to edit
</div>
) : isSubflow ? (
<SubflowEditor
currentBlock={currentBlock}
currentBlockId={currentBlockId}
subBlocksRef={subBlocksRef}
connectionsHeight={connectionsHeight}
isResizing={isResizing}
hasIncomingConnections={hasIncomingConnections}
incomingConnections={incomingConnections}
handleConnectionsResizeMouseDown={handleConnectionsResizeMouseDown}
toggleConnectionsCollapsed={toggleConnectionsCollapsed}
userCanEdit={canEditBlock}
isConnectionsAtMinHeight={isConnectionsAtMinHeight}
/>
) : (
<div className='flex flex-1 flex-col overflow-hidden pt-[0px]'>
{/* Subblocks Section */}
<div
ref={subBlocksRef}
className='subblocks-section flex flex-1 flex-col overflow-hidden'
>
<div className='flex-1 overflow-y-auto overflow-x-hidden px-2 pt-3 pb-2 [overflow-anchor:none]'>
{/* Workflow Preview - only for workflow blocks with a selected child workflow */}
{isWorkflowBlock && childWorkflowId && (
<>
<div className='subblock-content flex flex-col gap-[9.5px]'>
<div className='pl-0.5 font-medium text-[var(--text-primary)] text-small leading-none'>
Workflow Preview
</div>
)
})}
{hasAdvancedOnlyFields && canEditBlock && (
<div className='flex items-center gap-2.5 px-0.5 pt-3.5 pb-3'>
<div className='h-[1.25px] flex-1' style={DASHED_DIVIDER_STYLE} />
<button
type='button'
onClick={handleToggleAdvancedMode}
className='flex items-center gap-1.5 whitespace-nowrap font-medium text-[var(--text-secondary)] text-small hover-hover:text-[var(--text-primary)]'
>
{displayAdvancedOptions
? 'Hide additional fields'
: 'Show additional fields'}
<ChevronDown
className={`size-[14px] transition-transform duration-200 ${displayAdvancedOptions ? 'rotate-180' : ''}`}
/>
</button>
<div className='h-[1.25px] flex-1' style={DASHED_DIVIDER_STYLE} />
</div>
)}
{hasAdvancedOnlyFields && !canEditBlock && displayAdvancedOptions && (
<div className='flex items-center gap-2.5 px-0.5 pt-3.5 pb-3'>
<div className='h-[1.25px] flex-1' style={DASHED_DIVIDER_STYLE} />
<span className='whitespace-nowrap font-medium text-[var(--text-secondary)] text-small'>
Additional fields
</span>
<div className='h-[1.25px] flex-1' style={DASHED_DIVIDER_STYLE} />
</div>
)}
{advancedOnlySubBlocks.map((subBlock, index) => {
const stableKey = getSubBlockStableKey(
currentBlockId || '',
subBlock,
subBlockState
)
return (
<div key={stableKey} className='subblock-row'>
<SubBlock
blockId={currentBlockId}
config={subBlock}
isPreview={false}
subBlockValues={subBlockState}
disabled={!canEditBlock}
allowExpandInPreview={false}
isSearchHighlighted={
activeSearchTarget?.blockId === currentBlockId &&
(activeSearchTarget.subBlockId === subBlock.id ||
activeSearchTarget.canonicalSubBlockId ===
(subBlock.canonicalParamId ?? subBlock.id))
}
activeSearchTarget={activeSearchTargetForCurrentBlock}
/>
{index < advancedOnlySubBlocks.length - 1 && (
<FieldDivider subblockMarker />
<div className='relative h-[160px] overflow-hidden rounded-sm border border-[var(--border)]'>
{isLoadingChildWorkflow ? (
<div className='flex h-full items-center justify-center bg-[var(--surface-3)]'>
<Loader className='size-5 text-[var(--text-tertiary)]' animate />
</div>
) : childWorkflowState ? (
<>
<div className='[&_*:active]:!cursor-grabbing [&_*]:!cursor-grab [&_.react-flow__handle]:!hidden h-full w-full'>
<PreviewWorkflow
workflowState={childWorkflowState}
height={160}
width='100%'
isPannable={true}
defaultZoom={0.6}
fitPadding={0.15}
cursorStyle='grab'
lightweight
/>
</div>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
type='button'
variant='ghost'
onClick={handleOpenChildWorkflow}
className='absolute right-[6px] bottom-1.5 z-10 size-[24px] cursor-pointer border border-[var(--border)] bg-[var(--surface-2)] p-0 hover-hover:bg-[var(--surface-4)]'
>
<ExternalLink className='size-[12px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>Open workflow</Tooltip.Content>
</Tooltip.Root>
</>
) : (
<div className='flex h-full items-center justify-center bg-[var(--surface-3)]'>
<span className='text-[var(--text-tertiary)] text-small'>
Unable to load preview
</span>
</div>
)}
</div>
)
})}
</div>
)}
</div>
</div>
</div>
<FieldDivider subblockMarker />
</>
)}
{subBlocks.length === 0 && !isWorkflowBlock ? (
<div className='flex h-full items-center justify-center text-center text-[var(--text-placeholder)] text-small'>
This block has no subblocks
</div>
) : (
<div className='flex flex-col'>
{regularSubBlocks.map((subBlock, index) => {
const stableKey = getSubBlockStableKey(
currentBlockId || '',
subBlock,
subBlockState
)
const canonicalId = canonicalIndex.canonicalIdBySubBlockId[subBlock.id]
const canonicalGroup = canonicalId
? canonicalIndex.groupsById[canonicalId]
: undefined
const isCanonicalSwap = isCanonicalPair(canonicalGroup)
const canonicalMode =
canonicalGroup && isCanonicalSwap
? resolveCanonicalMode(
canonicalGroup,
blockSubBlockValues,
canonicalModeOverrides
)
: undefined
{/* Connections Section - Only show when there are connections */}
{hasIncomingConnections && (
<div
className={
'connections-section flex flex-shrink-0 flex-col overflow-hidden border-[var(--border)] border-t' +
(!isResizing ? ' transition-[height] duration-100 ease-out' : '')
}
style={{ height: `${connectionsHeight}px` }}
>
{/* Resize Handle */}
<div className='relative'>
<div
role='separator'
aria-orientation='horizontal'
className='absolute top-[-4px] right-0 left-0 z-30 h-[8px] cursor-ns-resize'
onMouseDown={handleConnectionsResizeMouseDown}
/>
const showDivider =
index < regularSubBlocks.length - 1 ||
(!hasAdvancedOnlyFields && index < subBlocks.length - 1)
return (
<div key={stableKey} className='subblock-row'>
<SubBlock
blockId={currentBlockId}
config={subBlock}
isPreview={false}
subBlockValues={subBlockState}
disabled={!canEditBlock}
allowExpandInPreview={false}
isSearchHighlighted={
activeSearchTarget?.blockId === currentBlockId &&
(activeSearchTarget.subBlockId === subBlock.id ||
activeSearchTarget.canonicalSubBlockId ===
(subBlock.canonicalParamId ?? subBlock.id))
}
canonicalToggle={
isCanonicalSwap && canonicalMode && canonicalId
? {
mode: canonicalMode,
disabled: !canEditBlock,
onToggle: () => {
if (!currentBlockId) return
const nextMode =
canonicalMode === 'advanced' ? 'basic' : 'advanced'
collaborativeSetBlockCanonicalMode(
currentBlockId,
canonicalId,
nextMode
)
},
}
: undefined
}
/>
{showDivider && <FieldDivider subblockMarker />}
</div>
)
})}
{hasAdvancedOnlyFields && canEditBlock && (
<div className='flex items-center gap-2.5 px-0.5 pt-3.5 pb-3'>
<div className='h-[1.25px] flex-1' style={DASHED_DIVIDER_STYLE} />
<button
type='button'
onClick={handleToggleAdvancedMode}
className='flex items-center gap-1.5 whitespace-nowrap font-medium text-[var(--text-secondary)] text-small hover-hover:text-[var(--text-primary)]'
>
{displayAdvancedOptions
? 'Hide additional fields'
: 'Show additional fields'}
<ChevronDown
className={`size-[14px] transition-transform duration-200 ${displayAdvancedOptions ? 'rotate-180' : ''}`}
/>
</button>
<div className='h-[1.25px] flex-1' style={DASHED_DIVIDER_STYLE} />
</div>
)}
{hasAdvancedOnlyFields && !canEditBlock && displayAdvancedOptions && (
<div className='flex items-center gap-2.5 px-0.5 pt-3.5 pb-3'>
<div className='h-[1.25px] flex-1' style={DASHED_DIVIDER_STYLE} />
<span className='whitespace-nowrap font-medium text-[var(--text-secondary)] text-small'>
Additional fields
</span>
<div className='h-[1.25px] flex-1' style={DASHED_DIVIDER_STYLE} />
</div>
)}
{advancedOnlySubBlocks.map((subBlock, index) => {
const stableKey = getSubBlockStableKey(
currentBlockId || '',
subBlock,
subBlockState
)
return (
<div key={stableKey} className='subblock-row'>
<SubBlock
blockId={currentBlockId}
config={subBlock}
isPreview={false}
subBlockValues={subBlockState}
disabled={!canEditBlock}
allowExpandInPreview={false}
isSearchHighlighted={
activeSearchTarget?.blockId === currentBlockId &&
(activeSearchTarget.subBlockId === subBlock.id ||
activeSearchTarget.canonicalSubBlockId ===
(subBlock.canonicalParamId ?? subBlock.id))
}
/>
{index < advancedOnlySubBlocks.length - 1 && (
<FieldDivider subblockMarker />
)}
</div>
)
})}
</div>
)}
</div>
</div>
{/* Connections Header with Chevron */}
{/* Connections Section - Only show when there are connections */}
{hasIncomingConnections && (
<div
className='flex flex-shrink-0 cursor-pointer items-center gap-2 px-2.5 pt-[5px] pb-[5px]'
onClick={toggleConnectionsCollapsed}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
toggleConnectionsCollapsed()
}
}}
role='button'
tabIndex={0}
aria-label={
isConnectionsAtMinHeight ? 'Expand connections' : 'Collapse connections'
className={
'connections-section flex flex-shrink-0 flex-col overflow-hidden border-[var(--border)] border-t' +
(!isResizing ? ' transition-[height] duration-100 ease-out' : '')
}
style={{ height: `${connectionsHeight}px` }}
>
<ChevronUp
className={
'size-[14px] transition-transform' +
(!isConnectionsAtMinHeight ? ' rotate-180' : '')
}
/>
<div className='font-medium text-[var(--text-primary)] text-small'>Connections</div>
</div>
{/* Resize Handle */}
<div className='relative'>
<div
role='separator'
aria-orientation='horizontal'
className='absolute top-[-4px] right-0 left-0 z-30 h-[8px] cursor-ns-resize'
onMouseDown={handleConnectionsResizeMouseDown}
/>
</div>
{/* Connections Content - Always visible */}
<div className='flex-1 overflow-y-auto overflow-x-hidden px-1.5 pb-2'>
<ConnectionBlocks
connections={incomingConnections}
currentBlockId={currentBlock.id}
/>
{/* Connections Header with Chevron */}
<div
className='flex flex-shrink-0 cursor-pointer items-center gap-2 px-2.5 pt-[5px] pb-[5px]'
onClick={toggleConnectionsCollapsed}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
toggleConnectionsCollapsed()
}
}}
role='button'
tabIndex={0}
aria-label={
isConnectionsAtMinHeight ? 'Expand connections' : 'Collapse connections'
}
>
<ChevronUp
className={
'size-[14px] transition-transform' +
(!isConnectionsAtMinHeight ? ' rotate-180' : '')
}
/>
<div className='font-medium text-[var(--text-primary)] text-small'>
Connections
</div>
</div>
{/* Connections Content - Always visible */}
<div className='flex-1 overflow-y-auto overflow-x-hidden px-1.5 pb-2'>
<ConnectionBlocks
connections={incomingConnections}
currentBlockId={currentBlock.id}
/>
</div>
</div>
</div>
)}
</div>
)}
</div>
)}
</div>
)}
</div>
</ActiveSearchTargetProvider>
)
}
@@ -0,0 +1,38 @@
'use client'
import type React from 'react'
import { createContext, useContext } from 'react'
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
const ActiveSearchTargetContext = createContext<ActiveSearchTarget | null>(null)
interface ActiveSearchTargetProviderProps {
value: ActiveSearchTarget | null
children: React.ReactNode
}
/**
* Provides the active workflow-search target to the panel editor sub-block tree.
*
* @remarks
* The editor provides the target scoped to the currently edited block. Components
* that project sub-block values into synthetic sub-blocks (e.g. tool-input params)
* re-provide a transformed target so nested inputs receive the rewritten
* `subBlockId`/`valuePath`. Outside any provider (e.g. preview), consumers see
* `null`, which disables search highlighting.
*/
export function ActiveSearchTargetProvider({ value, children }: ActiveSearchTargetProviderProps) {
return (
<ActiveSearchTargetContext.Provider value={value}>
{children}
</ActiveSearchTargetContext.Provider>
)
}
/**
* Returns the active workflow-search target for the nearest editor scope, or
* `null` when no search target applies (no provider, or target outside scope).
*/
export function useActiveSearchTarget(): ActiveSearchTarget | null {
return useContext(ActiveSearchTargetContext)
}
@@ -18,7 +18,7 @@ import {
useFolderExpand,
useItemDrag,
useItemRename,
useSidebarDragContext,
useSidebarListContext,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
import { SIDEBAR_SCROLL_EVENT } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'
import {
@@ -51,25 +51,11 @@ const logger = createLogger('FolderItem')
interface FolderItemProps {
folder: FolderTreeNode
dragDisabled?: boolean
hoverHandlers?: {
onDragEnter?: (e: React.DragEvent<HTMLElement>) => void
onDragLeave?: (e: React.DragEvent<HTMLElement>) => void
}
onFolderClick?: (folderId: string, shiftKey: boolean, metaKey: boolean) => void
onDragStart?: () => void
onDragEnd?: () => void
}
export function FolderItem({
folder,
dragDisabled = false,
hoverHandlers,
onFolderClick,
onDragStart: onDragStartProp,
onDragEnd: onDragEndProp,
}: FolderItemProps) {
const { isAnyDragActive } = useSidebarDragContext()
export function FolderItem({ folder }: FolderItemProps) {
const { isAnyDragActive, dragDisabled, onFolderClick, onItemDragStart, onItemDragEnd } =
useSidebarListContext()
const params = useParams()
const router = useRouter()
const workspaceId = params.workspaceId as string
@@ -234,9 +220,9 @@ export function FolderItem({
e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2)
dragGhostRef.current = ghost
onDragStartProp?.()
onItemDragStart(folder.parentId)
},
[folder.id, folder.name, workspaceId, onDragStartProp]
[folder.id, folder.name, folder.parentId, workspaceId, onItemDragStart]
)
const {
@@ -254,8 +240,8 @@ export function FolderItem({
dragGhostRef.current = null
}
handleDragEndBase()
onDragEndProp?.()
}, [handleDragEndBase, onDragEndProp])
onItemDragEnd()
}, [handleDragEndBase, onItemDragEnd])
const {
isOpen: isContextMenuOpen,
@@ -362,7 +348,7 @@ export function FolderItem({
const isModifierClick = e.shiftKey || e.metaKey || e.ctrlKey
if (isModifierClick && onFolderClick) {
if (isModifierClick) {
e.preventDefault()
onFolderClick(folder.id, e.shiftKey, e.metaKey || e.ctrlKey)
return
@@ -496,7 +482,6 @@ export function FolderItem({
draggable={!isEditing && !dragDisabled && !effectiveLocked}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
{...hoverHandlers}
>
<ChevronRight
className={clsx(
@@ -17,7 +17,7 @@ import {
useContextMenu,
useItemDrag,
useItemRename,
useSidebarDragContext,
useSidebarListContext,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
import {
buildDragResources,
@@ -46,28 +46,19 @@ import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
interface WorkflowItemProps {
workflow: WorkflowMetadata
active: boolean
dragDisabled?: boolean
onWorkflowClick: (workflowId: string, shiftKey: boolean) => void
onDragStart?: () => void
onDragEnd?: () => void
}
/**
* WorkflowItem component displaying a single workflow with drag and selection support.
* Uses the item drag hook for unified drag behavior.
* Selection and drag callbacks come from the sidebar list context; uses the item drag
* hook for unified drag behavior.
*
* @param props - Component props
* @returns Workflow item with drag and selection support
*/
export function WorkflowItem({
workflow,
active,
dragDisabled = false,
onWorkflowClick,
onDragStart: onDragStartProp,
onDragEnd: onDragEndProp,
}: WorkflowItemProps) {
const { isAnyDragActive } = useSidebarDragContext()
export function WorkflowItem({ workflow, active }: WorkflowItemProps) {
const { isAnyDragActive, dragDisabled, onWorkflowClick, onItemDragStart, onItemDragEnd } =
useSidebarListContext()
const params = useParams()
const workspaceId = params.workspaceId as string
const selectedWorkflows = useFolderStore((state) => state.selectedWorkflows)
@@ -345,9 +336,9 @@ export function WorkflowItem({
e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2)
dragGhostRef.current = ghost
onDragStartProp?.()
onItemDragStart(workflow.folderId || null)
},
[workflow.id, workflow.name, workspaceId, onDragStartProp]
[workflow.id, workflow.name, workflow.folderId, workspaceId, onItemDragStart]
)
const {
@@ -365,8 +356,8 @@ export function WorkflowItem({
dragGhostRef.current = null
}
handleDragEndBase()
onDragEndProp?.()
}, [handleDragEndBase, onDragEndProp])
onItemDragEnd()
}, [handleDragEndBase, onItemDragEnd])
const handleDoubleClick = useCallback(
(e: React.MouseEvent) => {
@@ -8,11 +8,11 @@ import { EmptyAreaContextMenu } from '@/app/workspace/[workspaceId]/w/components
import { FolderItem } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item'
import { WorkflowItem } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item'
import {
SidebarDragContext,
SidebarListContext,
useContextMenu,
useDragDrop,
useFolderSelection,
useSidebarDragContextValue,
useSidebarListContextValue,
useWorkflowSelection,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
import {
@@ -34,8 +34,6 @@ interface WorkflowListProps {
regularWorkflows: WorkflowMetadata[]
isLoading?: boolean
canReorder?: boolean
handleFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void
fileInputRef: React.RefObject<HTMLInputElement | null>
scrollContainerRef: React.RefObject<HTMLDivElement | null>
onCreateWorkflow?: () => void
onCreateFolder?: () => void
@@ -71,8 +69,6 @@ export const WorkflowList = memo(function WorkflowList({
regularWorkflows,
isLoading = false,
canReorder = true,
handleFileChange,
fileInputRef,
scrollContainerRef,
onCreateWorkflow,
onCreateFolder,
@@ -110,8 +106,6 @@ export const WorkflowList = memo(function WorkflowList({
handleDragEnd,
} = useDragDrop({ disabled: !canReorder })
const dragContextValue = useSidebarDragContextValue(isDragging)
useEffect(() => {
if (scrollContainerRef.current) {
setScrollContainer(scrollContainerRef.current)
@@ -347,6 +341,15 @@ export const WorkflowList = memo(function WorkflowList({
folderDescendantIds,
})
const listContextValue = useSidebarListContextValue({
isAnyDragActive: isDragging,
dragDisabled,
onWorkflowClick: handleWorkflowClick,
onFolderClick: handleFolderClick,
onItemDragStart: handleDragStart,
onItemDragEnd: handleDragEnd,
})
const isWorkflowActive = useCallback((wfId: string) => wfId === workflowId, [workflowId])
useEffect(() => {
@@ -377,28 +380,13 @@ export const WorkflowList = memo(function WorkflowList({
style={{ paddingLeft: `${level * TREE_SPACING.INDENT_PER_LEVEL}px` }}
{...createWorkflowDragHandlers(workflow.id, folderId)}
>
<WorkflowItem
workflow={workflow}
active={isWorkflowActive(workflow.id)}
dragDisabled={dragDisabled}
onWorkflowClick={handleWorkflowClick}
onDragStart={() => handleDragStart(folderId)}
onDragEnd={handleDragEnd}
/>
<WorkflowItem workflow={workflow} active={isWorkflowActive(workflow.id)} />
</div>
<DropIndicatorLine show={showAfter} level={level} position='after' />
</div>
)
},
[
dropIndicator,
isWorkflowActive,
dragDisabled,
createWorkflowDragHandlers,
handleWorkflowClick,
handleDragStart,
handleDragEnd,
]
[dropIndicator, isWorkflowActive, createWorkflowDragHandlers]
)
const renderFolderSection = useCallback(
@@ -457,13 +445,7 @@ export const WorkflowList = memo(function WorkflowList({
style={{ paddingLeft: `${level * TREE_SPACING.INDENT_PER_LEVEL}px` }}
{...createFolderDragHandlers(folder.id, parentFolderId)}
>
<FolderItem
folder={folder}
dragDisabled={dragDisabled}
onFolderClick={handleFolderClick}
onDragStart={() => handleDragStart(parentFolderId)}
onDragEnd={handleDragEnd}
/>
<FolderItem folder={folder} />
</div>
<DropIndicatorLine show={showAfter} level={level} position='after' />
@@ -493,13 +475,9 @@ export const WorkflowList = memo(function WorkflowList({
expandedFolders,
dropIndicator,
isDragging,
dragDisabled,
createFolderDragHandlers,
createEmptyFolderDropZone,
createFolderContentDropZone,
handleDragStart,
handleDragEnd,
handleFolderClick,
renderWorkflowItem,
]
)
@@ -565,7 +543,7 @@ export const WorkflowList = memo(function WorkflowList({
)
return (
<SidebarDragContext.Provider value={dragContextValue}>
<SidebarListContext.Provider value={listContextValue}>
<div
role='tree'
aria-label='Workflows'
@@ -612,15 +590,6 @@ export const WorkflowList = memo(function WorkflowList({
/>
)}
</div>
<input
ref={fileInputRef}
type='file'
accept='.json,.zip'
multiple
style={{ display: 'none' }}
onChange={handleFileChange}
/>
</div>
{onCreateWorkflow && onCreateFolder && (
@@ -635,6 +604,6 @@ export const WorkflowList = memo(function WorkflowList({
disableCreateFolder={disableCreate}
/>
)}
</SidebarDragContext.Provider>
</SidebarListContext.Provider>
)
})
@@ -10,10 +10,10 @@ export { useHoverMenu } from './use-hover-menu'
export { useItemDrag } from './use-item-drag'
export { useItemRename } from './use-item-rename'
export {
SidebarDragContext,
useSidebarDragContext,
useSidebarDragContextValue,
} from './use-sidebar-drag-context'
SidebarListContext,
useSidebarListContext,
useSidebarListContextValue,
} from './use-sidebar-list-context'
export { useSidebarResize } from './use-sidebar-resize'
export { useWorkflowOperations } from './use-workflow-operations'
export { useWorkflowSelection } from './use-workflow-selection'
@@ -31,6 +31,17 @@ type SiblingItem = {
createdAt: Date
}
/** Stable no-op drop-zone handlers returned when drag-and-drop is disabled. */
const NOOP_DRAG_HANDLERS = {
onDragOver: (e: React.DragEvent<HTMLElement>) => e.preventDefault(),
onDrop: (e: React.DragEvent<HTMLElement>) => e.preventDefault(),
onDragLeave: () => {},
}
const createNoopDragHandlers = () => NOOP_DRAG_HANDLERS
const noop = () => {}
/** Root folder vs root workflow scope: API/cache may use null or undefined for "no parent". */
function isSameFolderScope(
parentOrFolderId: string | null | undefined,
@@ -650,26 +661,20 @@ export function useDragDrop(options: UseDragDropOptions = {}) {
scrollContainerRef.current = element
}, [])
const noopDragHandlers = {
onDragOver: (e: React.DragEvent<HTMLElement>) => e.preventDefault(),
onDrop: (e: React.DragEvent<HTMLElement>) => e.preventDefault(),
onDragLeave: () => {},
}
if (disabled) {
return {
dropIndicator: null,
isDragging: false,
disabled: true,
setScrollContainer,
createWorkflowDragHandlers: () => noopDragHandlers,
createFolderDragHandlers: () => noopDragHandlers,
createEmptyFolderDropZone: () => noopDragHandlers,
createFolderContentDropZone: () => noopDragHandlers,
createRootDropZone: () => noopDragHandlers,
createEdgeDropZone: () => noopDragHandlers,
handleDragStart: () => {},
handleDragEnd: () => {},
createWorkflowDragHandlers: createNoopDragHandlers,
createFolderDragHandlers: createNoopDragHandlers,
createEmptyFolderDropZone: createNoopDragHandlers,
createFolderContentDropZone: createNoopDragHandlers,
createRootDropZone: createNoopDragHandlers,
createEdgeDropZone: createNoopDragHandlers,
handleDragStart: noop,
handleDragEnd: noop,
}
}
@@ -1,36 +0,0 @@
'use client'
import { createContext, useContext, useMemo } from 'react'
interface SidebarDragContextValue {
/** Whether any drag operation is currently in progress */
isAnyDragActive: boolean
}
/**
* Context for sharing drag state across sidebar components.
* Eliminates prop drilling of isAnyDragActive through component tree.
*/
export const SidebarDragContext = createContext<SidebarDragContextValue>({
isAnyDragActive: false,
})
/**
* Hook to access the sidebar drag state.
* Use this in WorkflowItem, FolderItem, etc. to check if any drag is in progress.
*
* @returns The current drag state
*/
export function useSidebarDragContext(): SidebarDragContextValue {
return useContext(SidebarDragContext)
}
/**
* Hook to create the sidebar drag context value.
*
* @param isDragging - Whether a drag is currently in progress
* @returns Context value to provide to SidebarDragContext.Provider
*/
export function useSidebarDragContextValue(isDragging: boolean): SidebarDragContextValue {
return useMemo(() => ({ isAnyDragActive: isDragging }), [isDragging])
}
@@ -0,0 +1,75 @@
'use client'
import { createContext, useContext, useMemo } from 'react'
interface SidebarListContextValue {
/** Whether any drag operation is currently in progress */
isAnyDragActive: boolean
/** Whether item dragging is disabled (e.g. viewer permissions) */
dragDisabled: boolean
/** Selects a workflow on click (single or shift-range selection) */
onWorkflowClick: (workflowId: string, shiftKey: boolean) => void
/** Selects a folder on modifier-click (shift-range or cmd/ctrl-toggle selection) */
onFolderClick: (folderId: string, shiftKey: boolean, metaKey: boolean) => void
/** Notifies the list that an item drag started from the given parent folder */
onItemDragStart: (parentFolderId: string | null) => void
/** Notifies the list that an item drag ended */
onItemDragEnd: () => void
}
const noop = () => {}
/**
* Context for sharing list-item interaction handlers and drag state across
* sidebar workflow-list components. Eliminates prop drilling of selection
* and drag callbacks into WorkflowItem/FolderItem.
*/
export const SidebarListContext = createContext<SidebarListContextValue>({
isAnyDragActive: false,
dragDisabled: false,
onWorkflowClick: noop,
onFolderClick: noop,
onItemDragStart: noop,
onItemDragEnd: noop,
})
/**
* Hook to access the sidebar list context.
* Use this in WorkflowItem, FolderItem, etc. for selection/drag callbacks and drag state.
*
* @returns The current sidebar list context value
*/
export function useSidebarListContext(): SidebarListContextValue {
return useContext(SidebarListContext)
}
/**
* Hook to create a memoized sidebar list context value.
*
* @param value - The handlers and drag state to expose to list items
* @returns Memoized context value to provide to SidebarListContext.Provider
*/
export function useSidebarListContextValue(
value: SidebarListContextValue
): SidebarListContextValue {
const {
isAnyDragActive,
dragDisabled,
onWorkflowClick,
onFolderClick,
onItemDragStart,
onItemDragEnd,
} = value
return useMemo(
() => ({
isAnyDragActive,
dragDisabled,
onWorkflowClick,
onFolderClick,
onItemDragStart,
onItemDragEnd,
}),
[isAnyDragActive, dragDisabled, onWorkflowClick, onFolderClick, onItemDragStart, onItemDragEnd]
)
}
@@ -1230,6 +1230,14 @@ export const Sidebar = memo(function Sidebar() {
className='hidden'
onChange={handleLogoFileChange}
/>
<input
ref={fileInputRef}
type='file'
accept='.json,.zip'
multiple
className='hidden'
onChange={handleImportFileChange}
/>
<div className='relative h-full'>
<aside
className='sidebar-container relative h-full overflow-hidden bg-[var(--surface-1)]'
@@ -1594,8 +1602,6 @@ export const Sidebar = memo(function Sidebar() {
regularWorkflows={regularWorkflows}
isLoading={isLoading}
canReorder={canEdit}
handleFileChange={handleImportFileChange}
fileInputRef={fileInputRef}
scrollContainerRef={scrollContainerRef}
onCreateWorkflow={handleCreateWorkflow}
onCreateFolder={handleCreateFolder}