Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d236cc8
refactor(tool-input): eliminate SyncWrappers, add canonical toggle an…
waleedlatif1 Feb 9, 2026
a0ebe08
fix(tool-input): restore optional indicator, fix folder selector and …
waleedlatif1 Feb 12, 2026
8af5617
add sibling values to subblock context since subblock store isn't rel…
waleedlatif1 Feb 12, 2026
a29afd2
cleanup
waleedlatif1 Feb 12, 2026
c43f502
fix(tool-input): render uncovered tool params alongside subblocks
waleedlatif1 Feb 12, 2026
41ed859
fix(tool-input): auto-refresh workflow inputs after redeploy
waleedlatif1 Feb 12, 2026
a25b26e
fix(tool-input): correct workflow selector visibility and tighten (op…
waleedlatif1 Feb 12, 2026
3e17627
fix(tool-input): align (optional) text to baseline instead of center
waleedlatif1 Feb 12, 2026
b65768b
fix(tool-input): increase top padding of expanded tool body
waleedlatif1 Feb 12, 2026
f707636
fix(tool-input): apply extra top padding only to SubBlock-first path
waleedlatif1 Feb 12, 2026
837a13e
fix(tool-input): increase gap between SubBlock params for visual clarity
waleedlatif1 Feb 12, 2026
54ed579
fix spacing and optional tag
waleedlatif1 Feb 12, 2026
b1cde02
update styling + move predeploy checks earlier for first time deploys
waleedlatif1 Feb 12, 2026
6ee73fa
update change detection to account for synthetic tool ids
waleedlatif1 Feb 12, 2026
030c61b
fix remaining blocks who had files visibility set to hidden
waleedlatif1 Feb 12, 2026
a76e6e9
cleanup
waleedlatif1 Feb 13, 2026
e15f3dc
add catch
waleedlatif1 Feb 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useCallback, useState } from 'react'
import { createLogger } from '@sim/logger'
import { runPreDeployChecks } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-predeploy-checks'
import { useNotificationStore } from '@/stores/notifications'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { mergeSubblockState } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'

const logger = createLogger('useDeployment')

Expand Down Expand Up @@ -35,6 +38,24 @@ export function useDeployment({
return { success: true, shouldOpenModal: true }
}

const { blocks, edges, loops, parallels } = useWorkflowStore.getState()
const liveBlocks = mergeSubblockState(blocks, workflowId)
const checkResult = runPreDeployChecks({
blocks: liveBlocks,
edges,
loops,
parallels,
workflowId,
})
if (!checkResult.passed) {
addNotification({
level: 'error',
message: checkResult.error || 'Pre-deploy validation failed',
workflowId,
})
return { success: false, shouldOpenModal: false }
}

setIsDeploying(true)
try {
const response = await fetch(`/api/workflows/${workflowId}/deploy`, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Button, Combobox } from '@/components/emcn/components'
import {
getCanonicalScopesForProvider,
getProviderIdFromServiceId,
getServiceConfigByProviderId,
OAUTH_PROVIDERS,
type OAuthProvider,
type OAuthService,
Expand All @@ -26,6 +27,11 @@ const getProviderIcon = (providerName: OAuthProvider) => {
}

const getProviderName = (providerName: OAuthProvider) => {
const serviceConfig = getServiceConfigByProviderId(providerName)
if (serviceConfig) {
return serviceConfig.name
}

const { baseProvider } = parseProvider(providerName)
const baseProviderConfig = OAUTH_PROVIDERS[baseProvider]

Expand Down Expand Up @@ -54,7 +60,7 @@ export function ToolCredentialSelector({
onChange,
provider,
requiredScopes = [],
label = 'Select account',
label,
serviceId,
disabled = false,
}: ToolCredentialSelectorProps) {
Expand All @@ -64,6 +70,7 @@ export function ToolCredentialSelector({
const { activeWorkflowId } = useWorkflowRegistry()

const selectedId = value || ''
const effectiveLabel = label || `Select ${getProviderName(provider)} account`

const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId])

Expand Down Expand Up @@ -203,7 +210,7 @@ export function ToolCredentialSelector({
selectedValue={selectedId}
onChange={handleComboboxChange}
onOpenChange={handleOpenChange}
placeholder={label}
placeholder={effectiveLabel}
disabled={disabled}
editable={true}
filterOptions={!isForeign}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
'use client'

import type React from 'react'
import { useRef, useState } from 'react'
import { ArrowLeftRight, ArrowUp } from 'lucide-react'
import { Button, Input, Label, Tooltip } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'

/**
* Props for a generic parameter with label component
*/
export interface ParameterWithLabelProps {
paramId: string
title: string
isRequired: boolean
visibility: string
wandConfig?: {
enabled: boolean
prompt?: string
placeholder?: string
}
canonicalToggle?: {
mode: 'basic' | 'advanced'
disabled?: boolean
onToggle?: () => void
}
disabled: boolean
isPreview: boolean
children: (wandControlRef: React.MutableRefObject<WandControlHandlers | null>) => React.ReactNode
}

/**
* Generic wrapper component for parameters that manages wand state and renders label + input
*/
export function ParameterWithLabel({
paramId,
title,
isRequired,
visibility,
wandConfig,
canonicalToggle,
disabled,
isPreview,
children,
}: ParameterWithLabelProps) {
const [isSearchActive, setIsSearchActive] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const searchInputRef = useRef<HTMLInputElement>(null)
const wandControlRef = useRef<WandControlHandlers | null>(null)

const isWandEnabled = wandConfig?.enabled ?? false
const showWand = isWandEnabled && !isPreview && !disabled

const handleSearchClick = (): void => {
setIsSearchActive(true)
setTimeout(() => {
searchInputRef.current?.focus()
}, 0)
}

const handleSearchBlur = (): void => {
if (!searchQuery.trim() && !wandControlRef.current?.isWandStreaming) {
setIsSearchActive(false)
}
}

const handleSearchChange = (value: string): void => {
setSearchQuery(value)
}

const handleSearchSubmit = (): void => {
if (searchQuery.trim() && wandControlRef.current) {
wandControlRef.current.onWandTrigger(searchQuery)
setSearchQuery('')
setIsSearchActive(false)
}
}

const handleSearchCancel = (): void => {
setSearchQuery('')
setIsSearchActive(false)
}

const isStreaming = wandControlRef.current?.isWandStreaming ?? false

return (
<div key={paramId} className='relative min-w-0 space-y-[6px]'>
<div className='flex items-center justify-between gap-[6px] pl-[2px]'>
<Label className='flex items-baseline gap-[6px] whitespace-nowrap font-medium text-[13px] text-[var(--text-primary)]'>
{title}
{isRequired && visibility === 'user-only' && <span className='ml-0.5'>*</span>}
</Label>
<div className='flex min-w-0 flex-1 items-center justify-end gap-[6px]'>
{showWand &&
(!isSearchActive ? (
<Button
variant='active'
className='-my-1 h-5 px-2 py-0 text-[11px]'
onClick={handleSearchClick}
>
Generate
</Button>
) : (
<div className='-my-1 flex min-w-[120px] max-w-[280px] flex-1 items-center gap-[4px]'>
<Input
ref={searchInputRef}
value={isStreaming ? 'Generating...' : searchQuery}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
handleSearchChange(e.target.value)
}
onBlur={(e: React.FocusEvent<HTMLInputElement>) => {
const relatedTarget = e.relatedTarget as HTMLElement | null
if (relatedTarget?.closest('button')) return
handleSearchBlur()
}}
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && searchQuery.trim() && !isStreaming) {
handleSearchSubmit()
} else if (e.key === 'Escape') {
handleSearchCancel()
}
}}
disabled={isStreaming}
className={cn(
'h-5 min-w-[80px] flex-1 text-[11px]',
isStreaming && 'text-muted-foreground'
)}
placeholder='Generate with AI...'
/>
<Button
variant='tertiary'
disabled={!searchQuery.trim() || isStreaming}
onMouseDown={(e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
}}
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
handleSearchSubmit()
}}
className='h-[20px] w-[20px] flex-shrink-0 p-0'
>
<ArrowUp className='h-[12px] w-[12px]' />
</Button>
</div>
))}
{canonicalToggle && !isPreview && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button
type='button'
className='flex h-[12px] w-[12px] flex-shrink-0 items-center justify-center bg-transparent p-0 disabled:cursor-not-allowed disabled:opacity-50'
onClick={canonicalToggle.onToggle}
disabled={canonicalToggle.disabled || disabled}
aria-label={
canonicalToggle.mode === 'advanced'
? 'Switch to selector'
: 'Switch to manual ID'
}
>
<ArrowLeftRight
className={cn(
'!h-[12px] !w-[12px]',
canonicalToggle.mode === 'advanced'
? 'text-[var(--text-primary)]'
: 'text-[var(--text-secondary)]'
)}
/>
</button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>
{canonicalToggle.mode === 'advanced'
? 'Switch to selector'
: 'Switch to manual ID'}
</p>
</Tooltip.Content>
</Tooltip.Root>
)}
</div>
</div>
<div className='relative w-full min-w-0'>{children(wandControlRef)}</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
'use client'

import { useEffect, useRef } from 'react'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
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'

interface ToolSubBlockRendererProps {
blockId: string
subBlockId: string
toolIndex: number
subBlock: BlockSubBlockConfig
effectiveParamId: string
toolParams: Record<string, string> | undefined
onParamChange: (toolIndex: number, paramId: string, value: string) => void
disabled: boolean
canonicalToggle?: {
mode: 'basic' | 'advanced'
disabled?: boolean
onToggle?: () => void
}
}

/**
* SubBlock types whose store values are objects/arrays/non-strings.
* tool.params stores strings (via JSON.stringify), so when syncing
* back to the store we parse them to restore the native shape.
*/
const OBJECT_SUBBLOCK_TYPES = new Set(['file-upload', 'table', 'grouped-checkbox-list'])

/**
* Bridges the subblock store with StoredTool.params via a synthetic store key,
* then delegates all rendering to SubBlock for full parity.
*/
export function ToolSubBlockRenderer({
blockId,
subBlockId,
toolIndex,
subBlock,
effectiveParamId,
toolParams,
onParamChange,
disabled,
canonicalToggle,
}: ToolSubBlockRendererProps) {
const syntheticId = `${subBlockId}-tool-${toolIndex}-${effectiveParamId}`
const [storeValue, setStoreValue] = useSubBlockValue(blockId, syntheticId)

const toolParamValue = toolParams?.[effectiveParamId] ?? ''
const isObjectType = OBJECT_SUBBLOCK_TYPES.has(subBlock.type)

const lastPushedToStoreRef = useRef<string | null>(null)
const lastPushedToParamsRef = useRef<string | null>(null)

useEffect(() => {
if (!toolParamValue && lastPushedToStoreRef.current === null) {
lastPushedToStoreRef.current = toolParamValue
lastPushedToParamsRef.current = toolParamValue
return
}
if (toolParamValue !== lastPushedToStoreRef.current) {
lastPushedToStoreRef.current = toolParamValue
lastPushedToParamsRef.current = toolParamValue

if (isObjectType && typeof toolParamValue === 'string' && toolParamValue) {
try {
const parsed = JSON.parse(toolParamValue)
if (typeof parsed === 'object' && parsed !== null) {
setStoreValue(parsed)
return
}
} catch {
// Not valid JSON — fall through to set as string
}
}
setStoreValue(toolParamValue)
}
}, [toolParamValue, setStoreValue, isObjectType])

useEffect(() => {
if (storeValue == null) return
const stringValue = typeof storeValue === 'string' ? storeValue : JSON.stringify(storeValue)
if (stringValue !== lastPushedToParamsRef.current) {
lastPushedToParamsRef.current = stringValue
lastPushedToStoreRef.current = stringValue
onParamChange(toolIndex, effectiveParamId, stringValue)
}
}, [storeValue, toolIndex, effectiveParamId, onParamChange])

const visibility = subBlock.paramVisibility ?? 'user-or-llm'
const isOptionalForUser = visibility !== 'user-only'

const config = {
...subBlock,
id: syntheticId,
...(isOptionalForUser && { required: false }),
}

return (
<SubBlock
blockId={blockId}
config={config}
isPreview={false}
disabled={disabled}
canonicalToggle={canonicalToggle}
dependencyContext={toolParams}
/>
)
}
Loading