mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-10 06:35:01 -05:00
Compare commits
1 Commits
cursor/dev
...
sim-609-to
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7210330f75 |
@@ -0,0 +1,410 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type React from 'react'
|
||||||
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
|
import { Combobox, Switch } from '@/components/emcn'
|
||||||
|
import {
|
||||||
|
CheckboxList,
|
||||||
|
Code,
|
||||||
|
DocumentSelector,
|
||||||
|
DocumentTagEntry,
|
||||||
|
FileSelectorInput,
|
||||||
|
FileUpload,
|
||||||
|
FolderSelectorInput,
|
||||||
|
KnowledgeBaseSelector,
|
||||||
|
KnowledgeTagFilters,
|
||||||
|
LongInput,
|
||||||
|
ProjectSelectorInput,
|
||||||
|
SheetSelectorInput,
|
||||||
|
ShortInput,
|
||||||
|
SlackSelectorInput,
|
||||||
|
SliderInput,
|
||||||
|
Table,
|
||||||
|
TimeInput,
|
||||||
|
WorkflowSelectorInput,
|
||||||
|
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components'
|
||||||
|
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 type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
|
||||||
|
import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
|
||||||
|
import { isPasswordParameter } from '@/tools/params'
|
||||||
|
|
||||||
|
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
|
||||||
|
previewContextValues?: Record<string, unknown>
|
||||||
|
wandControlRef?: React.MutableRefObject<WandControlHandlers | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a subblock component inside tool-input by bridging the subblock store
|
||||||
|
* with StoredTool.params via a synthetic store key.
|
||||||
|
*
|
||||||
|
* Replaces the 17+ individual SyncWrapper components that previously existed.
|
||||||
|
* Components read/write to the store at a synthetic ID, and two effects
|
||||||
|
* handle bidirectional sync with tool.params.
|
||||||
|
*/
|
||||||
|
export function ToolSubBlockRenderer({
|
||||||
|
blockId,
|
||||||
|
subBlockId,
|
||||||
|
toolIndex,
|
||||||
|
subBlock,
|
||||||
|
effectiveParamId,
|
||||||
|
toolParams,
|
||||||
|
onParamChange,
|
||||||
|
disabled,
|
||||||
|
previewContextValues,
|
||||||
|
wandControlRef,
|
||||||
|
}: ToolSubBlockRendererProps) {
|
||||||
|
const syntheticId = `${subBlockId}-tool-${toolIndex}-${effectiveParamId}`
|
||||||
|
const [storeValue, setStoreValue] = useSubBlockValue(blockId, syntheticId)
|
||||||
|
|
||||||
|
// Gate the component using the same dependsOn logic as SubBlock
|
||||||
|
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
|
||||||
|
disabled,
|
||||||
|
previewContextValues,
|
||||||
|
})
|
||||||
|
|
||||||
|
const toolParamValue = toolParams?.[effectiveParamId] ?? ''
|
||||||
|
|
||||||
|
/** Tracks the last value we wrote to the store from tool.params to avoid echo loops */
|
||||||
|
const lastInitRef = useRef<string>(toolParamValue)
|
||||||
|
/** Tracks the last value we synced back to tool.params from the store */
|
||||||
|
const lastSyncRef = useRef<string>(toolParamValue)
|
||||||
|
|
||||||
|
// Init effect: push tool.params value into the store when it changes externally
|
||||||
|
useEffect(() => {
|
||||||
|
if (toolParamValue !== lastInitRef.current) {
|
||||||
|
lastInitRef.current = toolParamValue
|
||||||
|
lastSyncRef.current = toolParamValue
|
||||||
|
setStoreValue(toolParamValue)
|
||||||
|
}
|
||||||
|
}, [toolParamValue, setStoreValue])
|
||||||
|
|
||||||
|
// Sync effect: when the store changes (user interaction), push back to tool.params
|
||||||
|
useEffect(() => {
|
||||||
|
if (storeValue == null) return
|
||||||
|
const stringValue = typeof storeValue === 'string' ? storeValue : JSON.stringify(storeValue)
|
||||||
|
if (stringValue !== lastSyncRef.current) {
|
||||||
|
lastSyncRef.current = stringValue
|
||||||
|
lastInitRef.current = stringValue
|
||||||
|
onParamChange(toolIndex, effectiveParamId, stringValue)
|
||||||
|
}
|
||||||
|
}, [storeValue, toolIndex, effectiveParamId, onParamChange])
|
||||||
|
|
||||||
|
// Initialize the store on first mount
|
||||||
|
const hasInitializedRef = useRef(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasInitializedRef.current && toolParamValue) {
|
||||||
|
hasInitializedRef.current = true
|
||||||
|
setStoreValue(toolParamValue)
|
||||||
|
}
|
||||||
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const configWithSyntheticId = { ...subBlock, id: syntheticId }
|
||||||
|
|
||||||
|
return renderSubBlockComponent({
|
||||||
|
blockId,
|
||||||
|
syntheticId,
|
||||||
|
config: configWithSyntheticId,
|
||||||
|
subBlock,
|
||||||
|
disabled: finalDisabled,
|
||||||
|
previewContextValues,
|
||||||
|
wandControlRef,
|
||||||
|
toolParamValue,
|
||||||
|
onParamChange: useCallback(
|
||||||
|
(value: string) => onParamChange(toolIndex, effectiveParamId, value),
|
||||||
|
[toolIndex, effectiveParamId, onParamChange]
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RenderContext {
|
||||||
|
blockId: string
|
||||||
|
syntheticId: string
|
||||||
|
config: BlockSubBlockConfig
|
||||||
|
subBlock: BlockSubBlockConfig
|
||||||
|
disabled: boolean
|
||||||
|
previewContextValues?: Record<string, unknown>
|
||||||
|
wandControlRef?: React.MutableRefObject<WandControlHandlers | null>
|
||||||
|
toolParamValue: string
|
||||||
|
onParamChange: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the appropriate component for a subblock type.
|
||||||
|
* Mirrors the switch cases in SubBlock's renderInput(), using
|
||||||
|
* the same component props pattern.
|
||||||
|
*/
|
||||||
|
function renderSubBlockComponent(ctx: RenderContext): React.ReactNode {
|
||||||
|
const {
|
||||||
|
blockId,
|
||||||
|
syntheticId,
|
||||||
|
config,
|
||||||
|
subBlock,
|
||||||
|
disabled,
|
||||||
|
previewContextValues,
|
||||||
|
wandControlRef,
|
||||||
|
toolParamValue,
|
||||||
|
onParamChange,
|
||||||
|
} = ctx
|
||||||
|
|
||||||
|
switch (subBlock.type) {
|
||||||
|
case 'short-input':
|
||||||
|
return (
|
||||||
|
<ShortInput
|
||||||
|
blockId={blockId}
|
||||||
|
subBlockId={syntheticId}
|
||||||
|
placeholder={subBlock.placeholder}
|
||||||
|
password={subBlock.password || isPasswordParameter(subBlock.id)}
|
||||||
|
config={config}
|
||||||
|
disabled={disabled}
|
||||||
|
wandControlRef={wandControlRef}
|
||||||
|
hideInternalWand={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'long-input':
|
||||||
|
return (
|
||||||
|
<LongInput
|
||||||
|
blockId={blockId}
|
||||||
|
subBlockId={syntheticId}
|
||||||
|
placeholder={subBlock.placeholder}
|
||||||
|
rows={subBlock.rows}
|
||||||
|
config={config}
|
||||||
|
disabled={disabled}
|
||||||
|
wandControlRef={wandControlRef}
|
||||||
|
hideInternalWand={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'dropdown':
|
||||||
|
return (
|
||||||
|
<Combobox
|
||||||
|
options={
|
||||||
|
(subBlock.options as { label: string; id: string }[] | undefined)
|
||||||
|
?.filter((option) => option.id !== '')
|
||||||
|
.map((option) => ({
|
||||||
|
label: option.label,
|
||||||
|
value: option.id,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
value={toolParamValue}
|
||||||
|
onChange={onParamChange}
|
||||||
|
placeholder={subBlock.placeholder || 'Select option'}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'switch':
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
checked={toolParamValue === 'true' || toolParamValue === 'True'}
|
||||||
|
onCheckedChange={(checked) => onParamChange(checked ? 'true' : 'false')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'code':
|
||||||
|
return (
|
||||||
|
<Code
|
||||||
|
blockId={blockId}
|
||||||
|
subBlockId={syntheticId}
|
||||||
|
placeholder={subBlock.placeholder}
|
||||||
|
language={subBlock.language}
|
||||||
|
generationType={subBlock.generationType}
|
||||||
|
value={typeof subBlock.value === 'function' ? subBlock.value({}) : undefined}
|
||||||
|
disabled={disabled}
|
||||||
|
wandConfig={
|
||||||
|
subBlock.wandConfig || {
|
||||||
|
enabled: false,
|
||||||
|
prompt: '',
|
||||||
|
placeholder: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wandControlRef={wandControlRef}
|
||||||
|
hideInternalWand={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'channel-selector':
|
||||||
|
case 'user-selector':
|
||||||
|
return (
|
||||||
|
<SlackSelectorInput
|
||||||
|
blockId={blockId}
|
||||||
|
subBlock={config}
|
||||||
|
disabled={disabled}
|
||||||
|
previewContextValues={previewContextValues}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'project-selector':
|
||||||
|
return (
|
||||||
|
<ProjectSelectorInput
|
||||||
|
blockId={blockId}
|
||||||
|
subBlock={config}
|
||||||
|
disabled={disabled}
|
||||||
|
previewContextValues={previewContextValues}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'file-selector':
|
||||||
|
return (
|
||||||
|
<FileSelectorInput
|
||||||
|
blockId={blockId}
|
||||||
|
subBlock={config}
|
||||||
|
disabled={disabled}
|
||||||
|
previewContextValues={previewContextValues}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'sheet-selector':
|
||||||
|
return (
|
||||||
|
<SheetSelectorInput
|
||||||
|
blockId={blockId}
|
||||||
|
subBlock={config}
|
||||||
|
disabled={disabled}
|
||||||
|
previewContextValues={previewContextValues}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'folder-selector':
|
||||||
|
return (
|
||||||
|
<FolderSelectorInput
|
||||||
|
blockId={blockId}
|
||||||
|
subBlock={config}
|
||||||
|
disabled={disabled}
|
||||||
|
previewContextValues={previewContextValues}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'knowledge-base-selector':
|
||||||
|
return <KnowledgeBaseSelector blockId={blockId} subBlock={config} disabled={disabled} />
|
||||||
|
|
||||||
|
case 'document-selector':
|
||||||
|
return (
|
||||||
|
<DocumentSelector
|
||||||
|
blockId={blockId}
|
||||||
|
subBlock={config}
|
||||||
|
disabled={disabled}
|
||||||
|
previewContextValues={previewContextValues}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'document-tag-entry':
|
||||||
|
return (
|
||||||
|
<DocumentTagEntry
|
||||||
|
blockId={blockId}
|
||||||
|
subBlock={config}
|
||||||
|
disabled={disabled}
|
||||||
|
previewContextValues={previewContextValues}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'knowledge-tag-filters':
|
||||||
|
return (
|
||||||
|
<KnowledgeTagFilters
|
||||||
|
blockId={blockId}
|
||||||
|
subBlock={config}
|
||||||
|
disabled={disabled}
|
||||||
|
previewContextValues={previewContextValues}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'table':
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
blockId={blockId}
|
||||||
|
subBlockId={syntheticId}
|
||||||
|
columns={subBlock.columns ?? []}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'slider':
|
||||||
|
return (
|
||||||
|
<SliderInput
|
||||||
|
blockId={blockId}
|
||||||
|
subBlockId={syntheticId}
|
||||||
|
min={subBlock.min}
|
||||||
|
max={subBlock.max}
|
||||||
|
step={subBlock.step}
|
||||||
|
integer={subBlock.integer}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'checkbox-list':
|
||||||
|
return (
|
||||||
|
<CheckboxList
|
||||||
|
blockId={blockId}
|
||||||
|
subBlockId={syntheticId}
|
||||||
|
title={subBlock.title ?? ''}
|
||||||
|
options={subBlock.options as { label: string; id: string }[]}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'time-input':
|
||||||
|
return (
|
||||||
|
<TimeInput
|
||||||
|
blockId={blockId}
|
||||||
|
subBlockId={syntheticId}
|
||||||
|
placeholder={subBlock.placeholder}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'file-upload':
|
||||||
|
return (
|
||||||
|
<FileUpload
|
||||||
|
blockId={blockId}
|
||||||
|
subBlockId={syntheticId}
|
||||||
|
acceptedTypes={subBlock.acceptedTypes || '*'}
|
||||||
|
multiple={subBlock.multiple === true}
|
||||||
|
maxSize={subBlock.maxSize}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'combobox':
|
||||||
|
return (
|
||||||
|
<Combobox
|
||||||
|
options={((subBlock.options as { label: string; id: string }[] | undefined) || []).map(
|
||||||
|
(opt) => ({
|
||||||
|
label: opt.label,
|
||||||
|
value: opt.id,
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
value={toolParamValue}
|
||||||
|
onChange={onParamChange}
|
||||||
|
placeholder={subBlock.placeholder || 'Select option'}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'workflow-selector':
|
||||||
|
return <WorkflowSelectorInput blockId={blockId} subBlock={config} disabled={disabled} />
|
||||||
|
|
||||||
|
case 'oauth-input':
|
||||||
|
// OAuth inputs are handled separately by ToolCredentialSelector in the parent
|
||||||
|
return null
|
||||||
|
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<ShortInput
|
||||||
|
blockId={blockId}
|
||||||
|
subBlockId={syntheticId}
|
||||||
|
placeholder={subBlock.placeholder}
|
||||||
|
password={isPasswordParameter(subBlock.id)}
|
||||||
|
config={config}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -196,6 +196,8 @@ export interface SubBlockConfig {
|
|||||||
type: SubBlockType
|
type: SubBlockType
|
||||||
mode?: 'basic' | 'advanced' | 'both' | 'trigger' // Default is 'both' if not specified. 'trigger' means only shown in trigger mode
|
mode?: 'basic' | 'advanced' | 'both' | 'trigger' // Default is 'both' if not specified. 'trigger' means only shown in trigger mode
|
||||||
canonicalParamId?: string
|
canonicalParamId?: string
|
||||||
|
/** Controls parameter visibility in agent/tool-input context */
|
||||||
|
paramVisibility?: 'user-or-llm' | 'user-only' | 'llm-only' | 'hidden'
|
||||||
required?:
|
required?:
|
||||||
| boolean
|
| boolean
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
buildCanonicalIndex,
|
buildCanonicalIndex,
|
||||||
type CanonicalIndex,
|
type CanonicalIndex,
|
||||||
|
type CanonicalModeOverrides,
|
||||||
evaluateSubBlockCondition,
|
evaluateSubBlockCondition,
|
||||||
getCanonicalValues,
|
getCanonicalValues,
|
||||||
isCanonicalPair,
|
isCanonicalPair,
|
||||||
@@ -12,7 +13,10 @@ import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
|
|||||||
export {
|
export {
|
||||||
buildCanonicalIndex,
|
buildCanonicalIndex,
|
||||||
type CanonicalIndex,
|
type CanonicalIndex,
|
||||||
|
type CanonicalModeOverrides,
|
||||||
evaluateSubBlockCondition,
|
evaluateSubBlockCondition,
|
||||||
|
isCanonicalPair,
|
||||||
|
resolveCanonicalMode,
|
||||||
type SubBlockCondition,
|
type SubBlockCondition,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format'
|
import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format'
|
||||||
import {
|
import {
|
||||||
|
buildCanonicalIndex,
|
||||||
|
type CanonicalModeOverrides,
|
||||||
evaluateSubBlockCondition,
|
evaluateSubBlockCondition,
|
||||||
|
isCanonicalPair,
|
||||||
|
resolveCanonicalMode,
|
||||||
type SubBlockCondition,
|
type SubBlockCondition,
|
||||||
} from '@/lib/workflows/subblocks/visibility'
|
} from '@/lib/workflows/subblocks/visibility'
|
||||||
import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig as BlockSubBlockConfig, GenerationType } from '@/blocks/types'
|
||||||
import { safeAssign } from '@/tools/safe-assign'
|
import { safeAssign } from '@/tools/safe-assign'
|
||||||
import { isEmptyTagValue } from '@/tools/shared/tags'
|
import { isEmptyTagValue } from '@/tools/shared/tags'
|
||||||
import type { ParameterVisibility, ToolConfig } from '@/tools/types'
|
import type { OAuthConfig, ParameterVisibility, ToolConfig } from '@/tools/types'
|
||||||
import { getTool } from '@/tools/utils'
|
import { getTool } from '@/tools/utils'
|
||||||
|
|
||||||
const logger = createLogger('ToolsParams')
|
const logger = createLogger('ToolsParams')
|
||||||
@@ -64,6 +68,14 @@ export interface UIComponentConfig {
|
|||||||
mode?: 'basic' | 'advanced' | 'both' | 'trigger'
|
mode?: 'basic' | 'advanced' | 'both' | 'trigger'
|
||||||
/** The actual subblock ID this config was derived from */
|
/** The actual subblock ID this config was derived from */
|
||||||
actualSubBlockId?: string
|
actualSubBlockId?: string
|
||||||
|
/** Wand configuration for AI assistance */
|
||||||
|
wandConfig?: {
|
||||||
|
enabled: boolean
|
||||||
|
prompt: string
|
||||||
|
generationType?: GenerationType
|
||||||
|
placeholder?: string
|
||||||
|
maintainHistory?: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubBlockConfig {
|
export interface SubBlockConfig {
|
||||||
@@ -327,6 +339,7 @@ export function getToolParametersConfig(
|
|||||||
canonicalParamId: subBlock.canonicalParamId,
|
canonicalParamId: subBlock.canonicalParamId,
|
||||||
mode: subBlock.mode,
|
mode: subBlock.mode,
|
||||||
actualSubBlockId: subBlock.id,
|
actualSubBlockId: subBlock.id,
|
||||||
|
wandConfig: subBlock.wandConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -811,3 +824,196 @@ export function formatParameterLabel(paramId: string): string {
|
|||||||
// Simple case - just capitalize first letter
|
// Simple case - just capitalize first letter
|
||||||
return paramId.charAt(0).toUpperCase() + paramId.slice(1)
|
return paramId.charAt(0).toUpperCase() + paramId.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SubBlock IDs that are "structural" — they control tool routing or auth,
|
||||||
|
* not user-facing parameters. These are excluded from tool-input rendering
|
||||||
|
* unless they have an explicit paramVisibility set.
|
||||||
|
*/
|
||||||
|
const STRUCTURAL_SUBBLOCK_IDS = new Set(['operation', 'authMethod', 'destinationType'])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SubBlock types that represent auth/credential inputs handled separately
|
||||||
|
* by the tool-input OAuth credential selector.
|
||||||
|
*/
|
||||||
|
const AUTH_SUBBLOCK_TYPES = new Set(['oauth-input'])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SubBlock types that should never appear in tool-input context.
|
||||||
|
*/
|
||||||
|
const EXCLUDED_SUBBLOCK_TYPES = new Set([
|
||||||
|
'tool-input',
|
||||||
|
'skill-input',
|
||||||
|
'condition-input',
|
||||||
|
'eval-input',
|
||||||
|
'webhook-config',
|
||||||
|
'schedule-info',
|
||||||
|
'trigger-save',
|
||||||
|
'input-format',
|
||||||
|
'response-format',
|
||||||
|
'mcp-server-selector',
|
||||||
|
'mcp-tool-selector',
|
||||||
|
'mcp-dynamic-args',
|
||||||
|
'input-mapping',
|
||||||
|
'variables-input',
|
||||||
|
'messages-input',
|
||||||
|
'router-input',
|
||||||
|
'text',
|
||||||
|
])
|
||||||
|
|
||||||
|
export interface SubBlocksForToolInput {
|
||||||
|
toolConfig: ToolConfig
|
||||||
|
subBlocks: BlockSubBlockConfig[]
|
||||||
|
oauthConfig?: OAuthConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns filtered SubBlockConfig[] for rendering in tool-input context.
|
||||||
|
* Uses subblock definitions as the primary source of UI metadata,
|
||||||
|
* getting all features (wandConfig, rich conditions, dependsOn, etc.) for free.
|
||||||
|
*
|
||||||
|
* For blocks without paramVisibility annotations, falls back to inferring
|
||||||
|
* visibility from the tool's param definitions.
|
||||||
|
*/
|
||||||
|
export function getSubBlocksForToolInput(
|
||||||
|
toolId: string,
|
||||||
|
blockType: string,
|
||||||
|
currentValues?: Record<string, unknown>,
|
||||||
|
canonicalModeOverrides?: CanonicalModeOverrides
|
||||||
|
): SubBlocksForToolInput | null {
|
||||||
|
try {
|
||||||
|
const toolConfig = getTool(toolId)
|
||||||
|
if (!toolConfig) {
|
||||||
|
logger.warn(`Tool not found: ${toolId}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockConfigs = getBlockConfigurations()
|
||||||
|
const blockConfig = blockConfigs[blockType]
|
||||||
|
if (!blockConfig?.subBlocks?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const allSubBlocks = blockConfig.subBlocks as BlockSubBlockConfig[]
|
||||||
|
const canonicalIndex = buildCanonicalIndex(allSubBlocks)
|
||||||
|
|
||||||
|
// Build values for condition evaluation
|
||||||
|
const values = currentValues || {}
|
||||||
|
const valuesWithOperation = { ...values }
|
||||||
|
if (valuesWithOperation.operation === undefined) {
|
||||||
|
const parts = toolId.split('_')
|
||||||
|
valuesWithOperation.operation =
|
||||||
|
parts.length >= 3 ? parts.slice(2).join('_') : parts[parts.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a set of param IDs from the tool config for fallback visibility inference
|
||||||
|
const toolParamIds = new Set(Object.keys(toolConfig.params || {}))
|
||||||
|
const toolParamVisibility: Record<string, ParameterVisibility> = {}
|
||||||
|
for (const [paramId, param] of Object.entries(toolConfig.params || {})) {
|
||||||
|
toolParamVisibility[paramId] =
|
||||||
|
param.visibility ?? (param.required ? 'user-or-llm' : 'user-only')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track which canonical groups we've already included (to avoid duplicates)
|
||||||
|
const includedCanonicalIds = new Set<string>()
|
||||||
|
|
||||||
|
const filtered: BlockSubBlockConfig[] = []
|
||||||
|
|
||||||
|
for (const sb of allSubBlocks) {
|
||||||
|
// Skip excluded types
|
||||||
|
if (EXCLUDED_SUBBLOCK_TYPES.has(sb.type)) continue
|
||||||
|
|
||||||
|
// Skip trigger-mode-only subblocks
|
||||||
|
if (sb.mode === 'trigger') continue
|
||||||
|
|
||||||
|
// Determine the effective param ID (canonical or subblock id)
|
||||||
|
const effectiveParamId = sb.canonicalParamId || sb.id
|
||||||
|
|
||||||
|
// Resolve paramVisibility: explicit > inferred from tool params > skip
|
||||||
|
let visibility = sb.paramVisibility
|
||||||
|
if (!visibility) {
|
||||||
|
// Infer from structural checks
|
||||||
|
if (STRUCTURAL_SUBBLOCK_IDS.has(sb.id)) {
|
||||||
|
visibility = 'hidden'
|
||||||
|
} else if (AUTH_SUBBLOCK_TYPES.has(sb.type)) {
|
||||||
|
visibility = 'hidden'
|
||||||
|
} else if (
|
||||||
|
sb.password &&
|
||||||
|
(sb.id === 'botToken' || sb.id === 'accessToken' || sb.id === 'apiKey')
|
||||||
|
) {
|
||||||
|
// Auth tokens without explicit paramVisibility are hidden
|
||||||
|
// (they're handled by the OAuth credential selector or structurally)
|
||||||
|
// But only if they don't have a matching tool param
|
||||||
|
if (!toolParamIds.has(sb.id)) {
|
||||||
|
visibility = 'hidden'
|
||||||
|
} else {
|
||||||
|
visibility = toolParamVisibility[sb.id] || 'user-or-llm'
|
||||||
|
}
|
||||||
|
} else if (toolParamIds.has(effectiveParamId)) {
|
||||||
|
// Fallback: infer from tool param visibility
|
||||||
|
visibility = toolParamVisibility[effectiveParamId]
|
||||||
|
} else if (toolParamIds.has(sb.id)) {
|
||||||
|
visibility = toolParamVisibility[sb.id]
|
||||||
|
} else {
|
||||||
|
// SubBlock has no corresponding tool param — skip it
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by visibility: exclude hidden and llm-only
|
||||||
|
if (visibility === 'hidden' || visibility === 'llm-only') continue
|
||||||
|
|
||||||
|
// Evaluate condition against current values
|
||||||
|
if (sb.condition) {
|
||||||
|
const conditionMet = evaluateSubBlockCondition(
|
||||||
|
sb.condition as SubBlockCondition,
|
||||||
|
valuesWithOperation
|
||||||
|
)
|
||||||
|
if (!conditionMet) continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle canonical pairs: only include the active mode variant
|
||||||
|
const canonicalId = canonicalIndex.canonicalIdBySubBlockId[sb.id]
|
||||||
|
if (canonicalId) {
|
||||||
|
const group = canonicalIndex.groupsById[canonicalId]
|
||||||
|
if (group && isCanonicalPair(group)) {
|
||||||
|
if (includedCanonicalIds.has(canonicalId)) continue
|
||||||
|
includedCanonicalIds.add(canonicalId)
|
||||||
|
|
||||||
|
// Determine active mode
|
||||||
|
const mode = resolveCanonicalMode(group, valuesWithOperation, canonicalModeOverrides)
|
||||||
|
if (mode === 'advanced') {
|
||||||
|
// Find the advanced variant
|
||||||
|
const advancedSb = allSubBlocks.find((s) => group.advancedIds.includes(s.id))
|
||||||
|
if (advancedSb) {
|
||||||
|
filtered.push(advancedSb)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Include basic variant (current sb if it's the basic one)
|
||||||
|
if (group.basicId === sb.id) {
|
||||||
|
filtered.push(sb)
|
||||||
|
} else {
|
||||||
|
const basicSb = allSubBlocks.find((s) => s.id === group.basicId)
|
||||||
|
if (basicSb) {
|
||||||
|
filtered.push(basicSb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-canonical, non-hidden, condition-passing subblock
|
||||||
|
filtered.push(sb)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolConfig,
|
||||||
|
subBlocks: filtered,
|
||||||
|
oauthConfig: toolConfig.oauth,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting subblocks for tool input:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user